Characters FAQ

From HEWIKI
Jump to: navigation, search

Contents

How do I load all of a user's characters at once?

One of the features of the HeroEngine 2009 vol 1 is the conversion of Account/Character nodes into Arbitrary Root Nodes(Arbitrary root node) which may be loaded independently of a player connection. This resolved a long standing issue in character selection where all characters were automatically loaded by C++ at login to character selection. Depending on the game, loading all characters belonging to an account could be a performance issue at scale (causing unnecessary additional load on the database). Now the load of character (and even the account node) is under HSL control allowing you to control the behavior to implement the strategy

The issue is not that the association no longer works persay as its list of target IDs is correct, but rather than the "target" of the association is a character node that is not being loaded in character selection unless you "select" it. Selection attaches the root node hierarchies (account/character) to the player's connection node (new in v1.23) causing them to move between areas during travel as the player's connection is redirected.

The Hero's Journey team chose to override the player connection mechanics (for the character selection area) to continue loading all of the accounts characters during character selection.

If your design calls for displaying all of the characters simultaneously in character selection, then there are two basic options available:

Depending on the number of characters you allow a user to have on a single account, I would tend to favor the second for a design that allows lots of characters per account over the first (or use the clean engine mechanics as implemented).

Implementing your own connection mechanics involves creating a new class which inherits from _playerConnection and then implementing the GameSpecific override on the $ACCOUNT system node that factories the player connection node.

    // From hj_playerConnectionClassMethods
    method _RootNodeLoadedNotification( rootID as ID )
      // continue here
      root as NodeRef = rootID 
      where root
        is kindof _playerAccount
          // HJ still operates as needing all characters loaded in character selection area
          //   so we load them all with the account by querying the database (async) with a filter
          //   for characters and then requesting the load of all of them
          $ACCOUNT._AttachPlayerConnectionRootNode(me._accountID, root)
 
          if GetCharacterSelectionArea() = getAreaNumber()
            filter as Class _nodeQueryFilter
            filter._className = "_playerCharacter"
            QueryRootNodeChildren(root, root, me, filter)   // Async
          else
            // this would be a very wierd thing in HJ's logic
            $ACCOUNT._SessionStarted( root )
            $ACCOUNT._EnteredArea( root )
            ScriptErrorAndContinue( "HJ shouldn't be getting here." )
          .
        .
        is kindof _playerCharacter
          when me.hj_playerConnectionState
            is LOADINGALLCHARACTERS
              AddAssociation( me._accountID, "AccountCharacterAssociation", root )
              // Remove the current loaded root from the pending list
              loop i from me.pendingPlayerCharacterLoads.length to 1 by -1
                if me.pendingPlayerCharacterLoads[i] = root
                  remove me.pendingPlayerCharacterLoads at i
                .
              .
 
              allCharactersLoaded as Boolean
              if me.pendingPlayerCharacterLoads.length = 0
                allCharactersLoaded = true
              .
 
              if allCharactersLoaded
                $ACCOUNT._SessionStarted( me._accountID )
 
                account as NodeRef of Class HJplayer_account = me._accountID
                var char = account.GetLastSelectedCharacter()
                if char != None
                  $ACCOUNT._AttachPlayerConnectionRootNode( account, char )
                .               
                $ACCOUNT._EnteredArea( me._accountID )
                me.hj_playerConnectionState = READY
 
                PlayerLoadFinished( me._accountID )
              .
            .
            is READY
              authoritativeName as String = LookupCharacterNameByCharacterID(rootID)  // character name based on authoritiative unique key in database
              connectionCharacterName as String = root.name
              if tolower( connectionCharacterName ) <> tolower( authoritativeName )
                connectionCharacterName = authoritativeName
              .
 
              $ACCOUNT._AttachPlayerConnectionRootNode(me._accountID, root)
              SetPlayerConnectionCharacterName(me._accountID, connectionCharacterName )
 
              $CHARACTERSELECTIONSYSTEM._OnSelectedCharacterLoad(me._accountID, rootID)
            .
          .
        .
        default
          me._KickPlayerConnection( "Unable to load player", "Loaded God knows what root node when trying to load account and character")
        .
      .
    .
    method _GotRootNodeChildren(requestID as ID, children as LookupList indexed by ID of String)
 
      // TODO: Probably should verify request ID is a character selection query before loading characters
      me.hj_playerConnectionState = LOADINGALLCHARACTERS
 
      if children.length = 0
        PlayerLoadFinished( me._accountID )
      .
      foreach child in children
        add back child to me.pendingPlayerCharacterLoads
        $ACCOUNT._LoadRootNode( child, me )
      .
    .
    method _RootNodeQueryFailure(requestID as ID, failureReason as String)
      // Oh nos, cannot validate character list
      ScriptError("Error trying to query account rootnode children when attempting to load the characters: " + failureReason)
    .
    method _KickPlayerConnection( playerMsg as String, logMsg as String )
 
      allowKick as Boolean
      when tolower( me.account_name )
        is "he-admin"
          allowKick = false
        .
        is "simu-cwiss"
          allowKick = false
        .
        is "simu-mike"
          allowKick = false
        .
        is "simu-skippy"
          allowKick = false
        .
        default
          allowKick = true
        .
      .
      allowKick = false
 
      if allowKick
        KickPlayer( me._accountID, playerMsg, logMsg )
      else
        //TODO: reimplement this once everything has been settled out of the new character connection deploy
    //    ScriptErrorAndContinue( "Told to KickPlayer for account(" + me.account_name + ") but the connection said no..." )
      .
    .

 
    // From HJ's $ACCOUNT game specific override
    //    this unloads the extra characters as the connection exits player selection area
    //    extra is defined as those characters that are not attached to the connection
    method HE_PostExitingArea( theAccount as NodeRef )
      account as NodeRef of Class _playerAccount = theAccount
      foreach assoc in QueryAssociation( account, "AccountCharacterAssociation", None )
        t as NodeRef of Class _playerCharacter = assoc.target
        // unattached character, so unload it
        if t <> None and IsRootNode( t ) and GetRootNodePlayerConnection( t ) = 0
          UnloadRootNode( t, me )
        .
      . 
    <snip...>
(does the code sample need to be updated, for example with HJplayer_account?)


Please note, it is strongly recommended that you override the _KickPlayerConnection() in your connection class and use $ACCOUNT._KickPlayer() instead of calling the external function directly to not disconnect at least a few of your developer accounts as well as the he-admin account. This ensures if you make an mistake in modifying the player connection mechanics that you can still log in to correct it.

How do I add a new character/creature to the engine?

Reference Information:

Creating characters
Defining a character in the Repository
Add Creature to Engine
Dynamic Character Primer

The rough outline for a brand new character/creature....

<specification>.DAT

The .DAT file tells the engine where to look for the model, animations and in the case of dynamic characters their parts/textures. For a specification MACharacter registered in the engine using /henpc, the engine expects to find a MACharacter.DAT file located in the path specified.

.dat

ANIMATIONSET.DAT

The animationset.dat file is a list of the sequences for a model. Using the Animation Panel to add new Sequences automatically updates this file. The animationset.dat file is expected to be located in the directory specified by the <specification>.DAT as the location where its animations will be found.

Defining An Animation Set

Animation Agent

The animation agent is specified by the <specification>.dat file. A minimalist animation agent would be something like:

channels
{
  AnimAllBody
}
 
inputs
{
  animation = Idle
}
 
action Default
{
  set(1) AnimAllBody
  {
    when animation {
    is Idle:
       anim "idle"
    DEFAULT:
       anim "idle"
    }
   hold 1
   blend 0.1
   align false
   looping true
  }
}

This minimalist agent of course would not be compatible with our character controller so you would not be able to "walk" around HJ Reference using this agent.

.aas

How can I access character data when the player is not connected?

Node Delivery Service Node Delivery Service

The node delivery service is fully implemented and deployed to licensees.

The first method that may suit your needs is what we call the Node Delivery Service. This mechanism allows you to "mail" a node (or hierarchy of nodes if children are hard associated to the parent) to a mailbox. The way you would use this mechanic is during logoff, you would mail whatever nodes you wanted to be available while the character is offline to your system area's mailbox. The system area would then load the envelope on demand when you needed information stored therein. After satisfying a request for information, the system area could mail the node (and its children) back to the system area's mailbox (thus unloading them from memory). When a character logs back in, you send a request to the system area to mail the envelope back to the character's mailbox.

Arbitrary Root Nodes

There are currently two fundamental root nodes that the database loads; players and areas. The arbitrary root node establishes a third type of root node that could be loaded into an area arbitrarily by script. This mechanic could be used to create additional node hierarchies to store data that is heavyweight or needs to be accessible when a player is not logged into the game. Hero's Journey will use this mechanic to store a variety of player data to which asynchronous access is acceptable (ie the root nodes will be loaded into a system area somewhere) and have relatively large data sets that we do not want to load with the character as they move between areas.


Extensible DLL Plugin Architecture

This mechanic allows a licensee to create C++ plugins and exposing new external functions to HeroScript.

Using the Extensible DLL Plugin Architecture, you can expose external functions that in turn communicate with any arbitrary data storage mechanism you want such as a database. Depending on your design, you might write updates to this external database when a character logs off. With the data stored externally to game data, you could query your database supporting (for example) website access to the information as well as in-game queries via external functions.

While we are maintaining your servers on our machines during the prototype stage, there is an issue with how your DLL is made available to the server as it will require our engineers handle the addition. Consequently, there will be some kind of process involved which may require you submit the source for our engineers to review. Of course as you move forward with your own machines, this will no longer be an issue as you will be able to add your DLLs for your servers directly.

How do I get the ID of a character node?

Getting the ID of your character node can be accomplished by:

  1. Writing an HSL /command
  2. Using the CLI
    1. First method
      1. Get your account ID by selecting your character with a selection tool and copying the ID from the "Properties Panel"
      2. In the console query the associations to your account node: \qa <nodeID> 0 0
      3. Look for a target node that has both the associations: AccountCharacterAssociation and base_hard_association. This will be your character node.
    2. Second method
      1. \qn <className> returns a list of all instantiations of that particular class in the area server's local GOM

How would I implement player stats (strength, dexterity, etc...)?

Character Statistics

This can be a very complex topic, or very simple dependent upon your game design. For the purposes of discussion I am going to assume a more complex model as it adequately exercises the solution(s) of the simple case.

Interfaces

Depending on the type of information you are requesting, a clean interface design leans towards a architecture where the information is retrieved in what may be an asynchronous manner. That means your "getter" methods may not return a value synchronously rather accept a nodef/scriptref to which they should report the value whenever it has been calculated/retrieved.

General Stats

Some of this discussion depends on your game design, for Hero's Journey we do not have the standard Strength/Dexterity/Intelligence model of stats so there is no sample code there for those particular concepts. You must first consider whether or not these types of stats are mutable or immutable values. For example, if you have a Diablo II style of game where part of character advancement has the addition of X points among your stats then you do not really gain (from a technical perspective) anything by having a spec for the values. Now you might consider having a spec that represents the initial values for a level 1 Barbarian for example, which would allow you to easily adjust them using the GUI change the values around.

If the values are immutable, then using specs or some similar data structure is probably ideal.

For the sake of discussion, I am going to assume you have an asymmetrical design for stats where characters "improve" their fully mutable stats during "level-up" and creatures use fixed values stored in a spec or plain old prototype. I would start by creating an abstract interface with methods for retrieving the various stats (getStrength(), getConstitution() and so on), with initially two child classes one for characters (class characterStatistics) and one for npcs (class npcStatistics). The major difference between the two would be that the character version would read field values directly from the character node while the npc version would retrieve those values from a spec minimizing the memory use for those values.

Stats.gif

Statistics that are player character specific for example experience, would only have fields in the class characterStats but potentially you would implement the method in the interface anyway simply overriding it to return 0 for npcs. This helps ensure the memory footprint of your npc data is as small as it can be, while allowing you to handle playercharacters and npcs in an identical manner for many of your script systems.

Combat Stats

For combat statistics such as hitpoints, we opted for Hero's Journey to encapsulate their handling within the class Combatant under the theory that stuff you do not want to be killable simply should not be valid targets for the combat system and thus there is no need for them to support the concept of hitpoints. This design choice helps eliminate the persistent MMO problem where players figure out how to do damage and kill npcs that you never intended to be killable. All of this can get fairly complicated as you add game effects that increase max/current health, requiring your method getCurrentHealth() to take those factors into account or be able to query a list of "modifiers" for whether or not they should modify the resulting value.

Our entire combat and abilities systems are then coded with the acting nodes cast as noderef of class Combatant handling player characters vs npcs identically from our combat mechanics perspective.

Again the preferred design here is that you create an interface that provides methods to get/set the information.

Giving Characters the Necessary Class(es)

Having created the necessary classes, now you need to figure out how to give those behaviors to characters/npcs. For npcs, see the discussion below on generic npc considerations. For player characters, new characters are created from a prototype specified by the game specific class methods script for the $CHARACTERSELECTION system node. Currently for your game, that is the script UIP_CharacterSelectionClassMethods in the HE_CSSUseCharacterPrototype method which specifies the prototype named UIP_Character. If you only want to have new characters created with your classes, then you can simply GLOM your statistic class onto the prototype. If you want all characters including those already created to have the classes, you can modify the inheritance of the UIP_Character_Class class to have additional parent classes (it is from this class that the UIP_Character prototype was created)

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox