(→How do I hide static assets?: updated images for Sapphire)
|Line 131:||Line 131:|
==How do I identify marshaled string data?==
==How do I identify marshaled string data?==
Revision as of 19:30, 29 October 2012
How do I find a specific function?
To find a certain function, log into HeroBlade, then open the Script Editor, either via the Tools menu or with CTRL-H. Once in the Script Editor, press CTRL-SHIFT-A to search through all scripts. Searches must be made through either Client Scripts, Server Scripts, or GUI XML scripts.
The search output will be in two columns. The righthand side will show the lines where that the text was found. The lefthand side will show the name of the script where the text was found, and which line it was on. To quickly open the script, double-click the line.
How do I use script templates?
Script "templates" are determined by the contents of a folder in the Organizer's Server/Client Scripts organizer. Any script added to the "Templates" folder will appear in the create a new script dialog box, and selection of the template will cause the contents of the selected script to be copied into your new one. You can achieve the exact same effect by opening a script and copying and pasting all of the code into your script.
How do I get the index value the selected item in a dropDownBox?
You can retrieve a list of values for a dropdown box using the _GUIDropDownBox class's method _getDropDownBoxValues(). The current value can be set/retrieved using _getDropDownBoxValue() or _setDropDownBoxValue(). DropDowns are set by value rather than index, but you can utilize the available methods to determine the index as the list of values returned is an ordered list.
How do I programmatically resize a guilabel to fit the text it contains?
If a guilabel's text contents change (or the guilabel is being set up via script), text often-times exceeds the boundaries of the guilabel due to insufficient dimensions. Often-times, it's necessary to resize the label to accommodate the new width of the text it contains. The external function 'external function GetGUILabelTextSize(GUIlabel as NodeRef) as Vector3' may be called on a guilabel to retrieve the unit width/height that would be required of the label to fully contain its text. This can then be used to set the width or height of the guilabel accordingly.
What does the external function GetGrounding() do?
external function GetGrounding(paramPos as Vector3,checkAbove as Float,walkSlope as Float,groundY references Float) as Boolean
Performs a raycast from the specified position attempting to collide with a surface upon which you can reasonably stand (based on the specified max walkslope). If it fails, it performs a secondary check using the position + checkabove in case the original position was perhaps slightly below the surface. It it successfully collides it returns (by reference) the height (groundY) and returns false if it failed to ground.
How do I create a smart object?
A "smart object" is simply an object that comes out of the library with behaviors beyond simple rendering. Whether its opening a door, or a trigger has behaviors built in for when a player enters it (via a GLOMmed class).
One type of smart object is created using the Stateful Object Specification Oracle which is used to instantiate objects that have built in state using the Area States System States System. These objects ultimately use the /hestates command to instantiate an area state for the object via a stateful object spec.
Any system to create a "smart object" utilizes the Asset Library's ability to issue commands in conjunction with the instantiation of asset instances Asset Library#New command. There are a variety of ways to write the command issued by the library, and ultimately the HSL code can parse the command to do whatever you want (even invoking a GUI to prompt the user for additional data). There is a common parsing scheme various of which I use which is implemented in the BaseSpec's method _parseTokensFromLibraryCmd() as well as in the commands /heglom $GLOM and /heassociations $ASSOCIATIONS library command option.
So, the first step is to decide the scope of the objects you plan to create. Are they sufficiently complex that you need a factory (probably a spec of some kind) to instantiate them properly? Is all that is needed is the addition of a class to the asset instances (if so, then /heglom may do everything you need)? Do the objects need to be associated to a particular data structure in the area (/heassociations may do what you need here)? Do you need user input to fully instantiate the objects (then you will want to launch a GUI for the users as a part of the library command)? Do your objects need to be aware of each other, users, creatures, other objects? Etc.
All of these are questions you have to resolve in the design for your smart objects. Sometimes you can leverage the existing MMO Foundation Framework (area states, etc) to achieve your needs, others you will probably need to design your own systems. Hero's Journey's Quest System for example relies on a host of "smart objects" tied together in an intricate web that allows the amazing flexibility to create virtually any quest imagineable via simple GUIs. It has smart objects that mark locations, objects that know how to be portals to quest instances, library icons that instantiate objects or launch UIs to create spawners/territories and so on.
While there isn't a tutorial on smart objects yet, there is a vast constellation of examples both in the MMO Foundation Framework and Hero's Journey Reference implementations. Ultimately, the smarts of an object depend on what your game design needs them to be.
Are assets created on the server side or the client side?
Representations of an Asset Instance (for instances that are part of an area's persisted geometry) exist both on server and client, though the representations are structurally different. The client instance is (typically) introduced to the client by way of a file (area.dat and <room>.dat) cached in the local repository cache to eliminate the bandwidth that would be otherwise required to tell the client about the geometry of an area. The files from the LRC are parsed and instances instantiated during the area loading phase of entering an area.
Asset Instances created using the prop bucket mechanic Prop buckets exist only on in the GOM of the local client in which they were instantiated.
Why am I getting the error "trying to destroy an indestructible node" when i destroy my subject?
The error is due to the fact that the ObsSubject/listener implementation assumes that you want the subject to "own" (i.e. be hard associated to) its listeners if they are not already "owned" by another node. This was done to help ensure that users of the system did not leak nodes unintentionally creating listeners and failing to clean them up when no longer needed (by definition the obsSubject's deletion deletes its child listeners).
The next release of the engine checks for indestructable nodes (nodes that are introduced and torn down explicitly by the engine, and which HSL is not allowed to destroy) and does not associate the listener with a hard association when it is indestructible.
Why am I getting the error "ERROR: Exceeded SYSTEM.EXEC.CPULIMIT" when I call my server-side method or function?
There are several possibilities. This error indicates that your method or function has retained control of the CPU for the limit predefined by the superglobal SYSTEM.EXEC.CPULIMIT. This can happen when a script gets stuck in an infinite loop or when a large amount of computations are performed synchronously without returning (effectively blocking the normal flow of execution). If you are experiencing this error in the midst of a legitimate computation, you can eliminate this error by increasing the value of the SYSTEM.EXEC.CPULIMIT variable as the first line of your method/function. Alternatively (and this is the recommended course of action), you can make use of asynchronous calls to reduce the amount of time the HeroMachine is blocked within your function.
When would the subject call the listener to destroy it?
There are two options:
- If you remove a listener via the removeListener() method of the subject and the subject "owns" (as denoted by being the source of a hard association to the target listener) then the subject calls the destroyListener() method on the listener object. If the listener is not owned by the subject, then it is not destroyed when removed.
- If you destroy the subject, and the listener is owned by that subject. The listener is destroyed automatically because of the hard association.
How do I prevent memory leaks?
One thing that makes a "leak" in a traditional sense less common in HeroEngine is the Association Engine. By virtue of the way that hard associations denote "ownership" of a node, when the owner is unloaded or destroyed all of the children owned by it are destroyed as well. So imagine you created a non-persisted node to store some transitory character information, and hard associated it to the character (because after all, it "belongs" to the character anyway). When the character unloads, the nonpersistent node is automatically cleaned up for you. We find in practice, that because the association engine is such a fundamental concept of the engine having a leak is rare. It should be noted, the only way HSL can leak memory is through the explicit instantiation of a node in the GOM.
In an upcoming release we intend to improve what I like to call the engine's ability for "Garbage Detection" and reporting. Currently, you have the capability to dump memory reports on both the server and client as well as monitoring of server processes via the Master Control Console which allow you to detect leaks but not track them down as easily as we would like. You should use DestroyNode() when you are done with a particular instantiation.
The default behavior for buttons is to forward their event up through their parent controls to the rootparent unless something has indicated the event was handled. This allows you to add buttons that do not specify a script or new class as you can simply catch the event in one of the parent controls (most of the time I handle button events at the rootparent level).
Args.handled should be set to true whenever you have done everything you wish to have done and you do not want the default behavior of buttons to continue onward (i.e. you do not want the event fowarded up to parent controls to catch if they choose).
How do I add headers to my specifications in a Spec Oracle?
The quickest (and easiest) way to add headers to your spec oracle is to use a chat command to execute the code on the server. Generally, most of developers create their own chat command to quickly execute arbitrary code on the server and/or client.
If you are using an evaluation world, you could simply use one of the existing commands (/cwiss is the chat command I created for doing these types of things, feel free to modify it).
shared function HE_ProcessCommandInput( account as NodeRef, input as String ) // The Command Handler calls this shared function based on a mapping stored on the COMMANDHANDLER prototype // which maps a /command to a script to call // args as List of String Tokenize( input, args ) if args.length < 2 HE_CommandUsage( account, input ) return partialMatch toLower( args ) to "addheader" var oracle = GetPrototype( "YourSpecOracle" ) oracle.AddCollectionHeader( "name", "Name", true ) default HE_CommandUsage( account, input )
What does the Where command do?
WHERE is used to cast a reference to a specific type when using a generic reference (untyped noderef), which you must do if you wish to access the member fields of the instance. The exception to this is in a class methods script, where the me node is implicitly typed to be of that class.
How do I change the position of a trigger instance from the client without having to make a remote server call?
Changes to the properties of an Instance on the client are by design local in nature, HeroBlade itself sends a message to the server to actually perform a "real" edit change. If you only want to change the local instance, then you can use the property interface or external function (faster) to do so.
postion as vector3 = somePosition trigger["Position"] = position // v1.23 supports vector3s natively in the script editor. In v1.22 you need to use "(0,0,0)", or pass it a vector3 variable
SetNodePosition( trigger, position)
Why can't I find an asset based on its name?
Not all instances have names. What you need to do is get the generic asset for triggers and then get all instances of triggers and find the one with the matching name.
theName as string = "whatever" theTrigger as NodeRef of Class TriggerInstance foreach instance in $EDIT._getGenericAreaAsset( "triggerasset" )._getAllInstancesOfAreaAsset() where instance is kindof TriggerInstance if tolower( instance.name ) = theName theTrigger = instance break . . . if theTrigger <> None $EDIT._RemoveInstance( theTrigger ) .
How do I hide static assets?
The filter's drop down allows you to control what objects you can see as well as what is selectable by the selection tools. When the stylized eye is present then you can see that type of object. When the pointer is present the selection tools will select that type of asset.
How do I identify marshaled string data?
There are two basic options for the identification of a marshalled string:
- context - by virtue of originating from a particular function it implies that the data you receive is what is expected
- supplemental data - a marshalled string can have accompanying data that provides the type. This is essentially what marshal utils does when it is includes the lists of classes that make up the node. Whether the supplemental data is included in the string or auxiliary to it is up to you to decide.
The fxspec class uses multiple lists of class because its list elements were always exactly of one particular class, so it worked just fine that way. If it had needed to allow for subclassing of various elements, then it would have needed to do something different (such as lists of string).
How do I delete a spec listing from the spec oracle?
Technically, you can not actual delete a spec without messing around in the internals of the system (by design). You can however mark a spec as "deleted" so that it does not show in the list of specs. Simply edit the spec and change the _specIsDeleted boolean to true and then save it.
How do I create area scripts?
Before HeroEngine was productized, HSL was only procedural in nature and used function calls in scripts for all game logic. As the engine evolved, we layered in Object Oriented programming into the language and it now mixes freely between the two. Area scripts date from the early days of the engine and are generally considered to be deprecated (though still functional) in favor of implementing your extension and override of the engine through System Nodes.
An area script is the script stored in the Script field on the root node for an area. You can retrieve the area root node using the external function GetRootNode(), it is recommended that you extend the $ACCOUNT system node to handle the behavior you want rather than using an area script.
How do I bookmark locations in code?
You can bookmark a location using cntrl-F2, or by clicking on the grey band on the left side of the editor.
Additionally, by using the Functions and Methods window you can quickly jump to a particular method/function in the current file.
Does HSL have a garbage collection system?
HSL is not a garbage collected language. Garbage collection as you know imposes a certain level of overhead on a system and we are not convinced that it is proper to impose that overhead on everyones' production games. What we plan to do is improve garbage detection so that it is easier to identify how and why you are leaking nodes. Garbage detection is something we feel could reasonably be left on in a development world and then disabled in your production worlds to avoid the overhead.
Does HSL have a have a control like a Rich Editor?
What is a "spec"?
A "spec" is short for specification. In the simplest sense a specification is a set of data to which you have convenient access to on both the server (read/write) and the client (read). A specification may exist purely as a data structure, or it may be used as a factory for a "lighter" weight object (a "specderivedobject") containing only mutable data (i.e. any data that changes during game play).
While a common usage of the spec system is to represent game data such as "items" or "abilities" (making a name like GameObject perhaps accurate), specs also commonly are used to represent other types of data such as the specification for a game's NPCs (knowing how to factory them; assigning them AI, making giving a "shopkeeper" its inventory, assembling the list of quests a "quest giver" distributes, and so on). The FX system's underlying data structures are "specs", which are used as templates for a factory to instantiate objects on the client to become a "wall of fire" or some other effect.
All persisted nodes instantiated by HSL are technically objects in the Database making DBObject probably the wrong name. Ultimately, it just happens to be the name we chose to use. :)
Examples of Hero's Journey's usage can be found it its Item, Ability and Npc Spec Oracles (oracle in this usage was meant to imply a person/thing that knows "stuff" such as the oracle at delphi, not the database) which you can find located in the Utilities (hotspot) Interface under the Tools tab.
How do I add action marks to the Hero Blade drop-down list?
Currently, it requires source code to add new types to the drop-down list because ideally you want the action mark to visualize properly for your new type as well.
However, that limitation does not stop you from using action marks for an infinite variety of uses because there is a "generic" type which in combination with the "ActionTag" allows you to via HSL script use the actionmark for things that are not supported directly by the C++. At its heart, Action Marks are used to specify a point and orientation in the game world from which to start a complex animation. The point and orientation can be used to navigate a character or creature to the right position to perform actions such as "kick the door".
How you use action marks in your game is totally up the HSL systems you write for your game, including whether or not a UI element like the button you see in Hero's Journey's implementation appears for them to click on.
How do I create a magic trap ability?
There are a few ways you can accomplish this:
- Spatial Awareness
- Polling for targets within the trap volume (GetTargetsInSphere(), GetTargetsInSphericalAnnulus(), GetTargetsInBox(), GetTargetsInCylinder(), GetTargetsInCylindricalAnnulus(), GetTargetsInCone())
Hero's Journey's ability system uses the polling method based on a timer for abilities like "Elemental Wall".
wall of fire
Now, Hero's Journey has an incredible ability system written by its developers that allows the creation of abilities in a data driven way that extrapolates the general functionality into decorators that can be mixed together to create almost every ability that we have been able to think of so far (and if they need something totally new, it is easy to plug in the new functionality. You can view the HJ ability system by opening the Utilities Interface Hotspot shift-F1. Navigate to the tools menu, and select the Ability Spec Oracle.
It is important to note, the HJ ability system is not part of the engine but was developed using HSL and the MMO Foundation Framework (included as a part of the engine) Spec System Spec Oracle and Spec System - Basic Usage.
What is the "%" symbol used for?
The % syntax indicates that the method RemoteDisplayTextBanner() will be called in the destination GOM (i.e. in this case on the client) on the system node TEXTBANNER. In other words, it is saying call the method on the remote system node. This syntax is used when remote calling from server to client, or client to server as it is possible to have system nodes with the same name in source and destination GOMs.
Does HSL support global variables?
Its difficult to answer this with a simple yes/no answer.
In the traditional programming understanding of a global variable, we do not have global variables in HeroScript. However, HeroEngine's Game Object Model GOM supports a variety of methods that can provide functionality to achieve a similar effect with the most prevalent being:
- System Nodes System nodes are singletons instantiated (from a prototype) in a local GOM. This mechanism exposes an easy-to-use way of exposing behaviors and data that must persist between script runs.
As far getting a reference to a node you created goes there are a number of options;
- Associations Associations
- fields that reference the node: ID or Noderef Node ref
- System Nodes are easily referenced in script via $NAME
- For persisted nodes, you can hard code the ID in your code (though it is not generally recommended for all of the usual reasons)
More information on the options for storing data can be found at:
How do I make the player log off?
The _playerAccountClassMethods._Exit() method on the server calls the client to instruct it to execute the external function CloseClient() resulting in a clean exit from the game.
Logoff (i.e. return to character selection) consists really only of initiating travel to return to the character selection area, at least insofar as the engine itself is concerned. In the next release which includes the new player connection mechanics (the connection is represented by a node) there will be a method exposed _playerConnectionLogoff(), but ultimately all that does is initiate travel to character selection by default.
How are objects created in HeroEngine?
There are a variety of ways that objects are created in HeroEngine; using the Editing Client (HeroBlade), explicit creation via script, and a hybrid (typically created by the Asset Library and a command Library#New_command that is created via the editing client and augmented by script.
How does HeroEngine handle the ordering of events?
There is no guarantee of order under any circumstance. While (currently) under the hood requests to the repository are implemented as a queue, we may need to change that implementation to one that uses a hashmap (or something else) in the future. Even if the implementation continues to be a queue, the fact that the file may not exist in your local repository would necessitate that you handle the possibility of unordered responses. You simply can not depend on the order.
If your implementation requires specific ordering, you could implement a queue in HSL to request all of the specs you wish to have and only generate events to the requesting systems/nodes in the order things were requested once you receive notice that they are all loaded.
What is the maximum string length in HSL?
There is no limit to the length of a string in HSL or the DOM/GOM, other than whatever limits might be imposed by your usage. For example, if you store a Gigabyte string in a node in an area your area server is going to use up a lot of RAM :) or if you transmit an enormous string your bandwidth limits may be exceeded and drown out replication traffic for an extended period.
How closely integrated to HeroEngine is HeroScript?
HeroEngine was created with the idea that you should be able to create your entire game without ever needing to touch source code, consequently HeroScript is a first class citizen in the engine with nearly complete control over every thing the engine does including fundamental mechanics like dynamic spinning up/down of processes to handle load as "services" or serve as "area servers/zones". The actual implementation of game logic as well as the structure or your game's data is 100% under your control as we have taken great pains to not limit what you can do with HeroEngine.
How easy is it to learn HeroScript as compared to other scripting languages, like Python or Lua?
As far as learning a new scripting language like HeroScript goes, you will find its very familiar to anyone used to a high level object oriented language and while like any language it has quirks it also is full of features that are specifically written with the creation of MMOs in mind (something you can not find in a general purpose scripting language). We have had licensee's produce prototypes they handed out to publishers to play in less than 90 days starting with absolutely no familiarity with the engine.
Comparing a general purpose language like Python or Lua to a language crafted to take full advantage of the architecture of a MMO engine like HeroEngine is not an easy one to make. As one would expect HeroScript has local variables, expressions with precedence, functions with parameters, supports polymorphism, inheritance, an easy to read syntax, a sophisticated IDE and a ton of other features we have come to expect from high level scripting languages. General purpose languages will often have modules already written that have functionality you want, HeroScript does not yet have that kind of community but we do provide access to a reference copy of our game Hero's Journey providing a very complete example of how you might implement any number of MMO systems. Python or Lua would almost certainly have less complete/integrated access to the HeroEngine's data object model, game object model and the amazingly dynamic collaborative environment they support. Developing your game using HeroScript means you can develop your game without ever restarting; content, data definitions, game data, and game logic all update dynamically on the fly.
HeroEngine supports the HeroScript Extension Interface HeroScript Extension Plugin which allows you to extend the scripting language's capabilities through C++ DLLs that can be loaded (and unloaded) on demand without ever needing to restart the client or server processes.
A high level overview of the language can be found on our documentation wiki at HSL for Programmers
How do I implement a custom cursor?
The cursor works like this:
Each frame, a call is made to the _cursorClassMethods script's function onFrameUpdate. This function then makes a call to a game specific override if one exists. This gives you the opportunity to specify a different cursor spec to use for the node (if none is specified it uses the default cursor spec). If a new spec is specified by calling the _requestCustomCursorSpec(), a request is made to asynchronously load the spec (if it is not already cached in memory). Once the spec is available, it sets the cursor to use the specification data to modify the cursor GUI (specifying new textures, sizes, etc).
So, the most basic type of extension for the system (other than specifying a different default) is to implement the HE_onCursorFrameUpdate() method to specify a different specID. In the simple case where you just want to display a different cursor for characters, you can simply implement the code to check for the Type of HBNode and use a new spec for characters (the sample code check uses is kindof _ACCControllerOwner instead of "Type" because it is faster). Hero's Journey uses a more complicated system that caches the id of the NodeUnderMouse and requests the server tell it what spec to use, this allows for complicated logic to determine which of our 25 cursors should be used. Complex behaviors in your cursor system require either you ask the server, or you have the server transmit sufficient knowledge that the client can make appropriate decisions (such as only change to the Interactable cursor for this acorn on the ground if the character is not currently in combat and is on Quest "Recover the Acorns").
So, here we go...
- Update your spec to include the size.x and size.y of your cursor
- Remove your override for HE_getCustomCursor(), you only need to implement this override if you are going to use a different name or gui layer for your cursor
- Implement an override for the frameupdate instead HE_onCursorFrameUpdate. I've included some sample code for your override
method HE_onCursorFrameUpdate(tInt as TimeInterval) as Boolean // // // nodeUnderMouse as NodeRef of Class HBNode = GetNodeUnderMouse() cursor as NodeRef of Class GUIControl = $Cursor._getCustomCursor() // Check to see if we need to reset to the default cursor // if already default, then no need to do anything if nodeUnderMouse = None if me._getCurrentCursorSpecKey() <> me._getCursorDefaultGameSpec() me._requestCustomCursorSpec( me._getCursorDefaultGameSpec() ) . else // If it is a character/npc, you could use the HBNode's property "Type" = Character. // However, the "is kindof" check is faster. if nodeUnderMouse is kindof _ACCControllerOwner if ( nodeUnderMouse["Selectable"] = true ) if me._getCurrentCursorSpecKey() <> 2 // Hover over Character's cursor spec me._requestCustomCursorSpec( 2 ) . . . . me._setLastNodeUnderCursor( nodeUnderMouse ) return false .
Why cant I access the parent instance of a camera?
Camera's do not use Parenting the way HBNodes normally function, rather they use a more flexible system customized for the way games use cameras through a series of external functions.
How do I get the camera to follow a node?
If you want a camera to follow a node, you use the External functions related to /SetCameraFollow()/ which include functions for adjusting the positional and rotational offsets. You can find a list of these functions in the _external functions script on the client by searching for Camera and on the wiki. There are "getter" functions for dealing with the offsets that would be used to get the rotation/position instead of using the properties.
What kind of scripting tools are there?
HeroScript Language (HSL) and the Data Object Model are the fundamental building blocks for implementation of your game systems and custom tools. Both of these are live, real-time and collaborative. Again, this is a pretty massive subject. The vast majority of this wiki is devoted to the power of HSL and the DOM.
The thing to keep in mind is these interdependent features are designed for the real-world needs of MMO development at a very deep level. The demonstrations you may have seen about how changes to game design can reflect easily as changes to identifier names throughout the DOM and script environment, are just one example.
Can I make objects at given positions in the world, from script?
Yes, HeroEngine definitely supports this kind of dynamic placement. There are several ways to accomplish this, but here is the most likely way:
Let's take one example: loot containers.
This is a two part issue. The first part is the representation on the server and the other is the representation on the client.
On the server, you'd probably create some sort of "Dynamic Loot System". This system would have an interface for creating a bunch of loot and picking random spots for it. So that probably means a class called "LootContainer" which keeps track of a position in the world (either in coordinates, or an index into a list of valid loot places, etc.), plus either fields to represent the content (or maybe content is represented by other nodes that associate with this or something else).
So on area spin up, you'd tell the Dynamic Loot System to do its thing and it would spin up a bunch of loot containers based on whatever criteria your design calls for.
Now, for the next part, we want the look to be represented on the client. In this case, we want to figure out the minimum representation needed. We probably don't need the clients to know more than they should (avoid cheating), so they definitely should not know what the loot containers have. It may even be questionable if we should tell the client the location of the containers until you get close enough to them... but for the sake of simplicity we'll ignore that issue.
So, one possible design is for the Dynamic Loot System on the server to have a partner system on the client. We'll call that the "DynamicLootOracle" and implement it using the Oracle (see: HSL Oracle Systems).
The DynamicLootOracle will get told a list of loot container locations from the Dynamic Loot System on the server. The server communicates this to the client via remote calls, packaging up the list of locations using marshaling .
Now, lets assume that for this design loot containers are all represented simply by a chest art asset. So what will happen here is that the DynamicLootOracle will, on initialization, create a Prop Bucket and load in the chest asset.
Now the DynamicLootOracle goes ahead and creates instances of the chest out of the prop bucket and places the chests based on the positions it was told by the server.
You might have it so a person needs to double-click on the chest to get its contents, so in that case you'd GLOM some class (say, "LootContainer_Client") that has methods that respond to the double click event. When that occurs, it would call methods on the DynamicLootOracle to send a remote call back to the server to the Dynamic Loot System which would then respond with information to populate whatever GUI you want to display to the user at that point (for example).
Detecting the double-click will be done via a method call.
One note: As you probably already know, a real implementation must check that the user was located near the chest when it is told about the double-click, to make sure there was no cheating going on.
There is a lot more you'd want to do for a full implementation, such as loot sharing, and of course this is all contingent on a full item and inventory system, which is beyond the scope of this one question! :)
How do you check if an ID is valid?
Q: In many scripting languages there is usually a function like IsIDValid(myID as ID), which checks to see if an object ID refers to an object that still exists. Does HSL have a similar function? For example, if there's a list of creatured stored in a List of ID, and it's desired to iterate through that list during a “re-initialize” function, and delete all existing creatures before respawning them.
A: Sure, doing an ID check is easy. Here's an example:
// ID Check bob as ID = 123456789 checkMe as NodeRef = bob if (checkMe == None) println("ID " + ID + " does not exist anymore!") .
This of course means the local GOM. It is not possible to check if the object exists in some other GOM (a different server) or in the database (not loaded anywhere) in this manner. But the above is probably what is needed. If not, please contact our technical staff.
Why does $ACCOUNT._OnlinePlayer() filter out some of the accounts internally by the isTimerSuspended() and isDeepTimerSuspended() methods?
HeroEngine's timers, while modeled after C#'s implementation of timers, have a few powerful concepts of particular use in MMOs:
- Ability to differentiate between elapsed game time and wall clock time
- Ability to be suspended (even recursively within a node hierarchy)
Timers on a suspended node to not fire while suspended, but remain aware that time is passing. Deep suspension suspends the node and all of its children (for example a character an all of the spell effects affecting him). The most common reason that timers are suspended is during the loading process (where a character loads into an area), during the load process there is a period in which the client may not yet be ready to process events. You might come up with other uses for suspension, for example suspending all of a user's spell effects while they are talking to a customer service representative.
How can I get the list of the current players on the client side?
By default, the client does not really know the difference between players and creatures. There is an external function GetMinimapCharacters() (which you can find the _ExternalFunctions script) to get a list of character (creature or player) of which the client is aware (IE has been told to create an HBNode to handle rendering them). HeroEngine functions in this manner because we did not want to transmit data to the client that for some types of games might be inappropriate for the client to know.
If your game needs to differentiate between players and creatures (or needs additional data beyond the simple rendering) you probably will want to implement a system similar to what is described in the Character Data tutorial Character Data Tutorial. A system such as the one described has advantages in that it is possible to have data for characters that are (for example) in different areas or perhaps even offline.
Is the style set in the hotspot GUI feature complete?
The Mission System provides a architecturally dated implementation that predates object oriented programming and many other powerful features subsequently added to HeroScript. While it does act as a relatively complete example of how one might implement such a system, the system itself has been deprecated in favor of the new Quest System that we use today both as the quest content creation tool for Hero's Journey and as a demonstration of using HeroEngine to do things that are impossible in other engines (dynamic collaborative content creation).
The fundamental concept present in either system is the use of a system area to store persisted data that must be accessible to other areas via remote calls. Each system associates (hard) the data nodes for the system to a "root" node, and the "root" node is in turn associated (hard) to the area root node to handle persistence to the database as a part of the area. As needed, each system may be using additional soft associations for ease of querying.
Is there a way to capture keyboard input in editor mode as there is for game mode?
There is a system node $KEYCAPTURE exists which may be extended to provide new keybinding commands for HeroBlade. Like all system nodes, you will need to create a game specific override class implementing (in this case) the method HE_OnCaptureKeyDown.
Why won't my string auto convert to a noderef?
Strings do autoconvert to noderefs provided the node exists in the local GOM in which you are performing the conversion. If the node does not exist, or if you cast the noderef to be of a class that the node is not kindof then it evaluates to NONE. Likewise, and ID will autoconvert to a noderef provided the node exists in the local GOM. String formatting for a vector3 is "(x,y,z)", if you do not have the string properly formatted it will not autoconvert.
Assume we have variable named fooNode which is a node of class foo with the ID 1234
valid uses of autoconversion:
s as string = fooNode i as id = s n as noderef = i
invalid uses of autoconversion:
// example 1 s as string = fooNode n as noderef of class BAR = s // this will result in n = NONE, because our node is not of class BAR // example 2 i as id = fooNode n as noderef of class BAR = i // this will also result in n = NONE, because our node is not of class BAR // example 3 // fooNode is a noderef on the client which we have not created a corresponding instance on the client // this code assumes we just received a remote call with args["fooNode"] = fooNode n as noderef of class foo = rmc.args["fooNode"] // this fails because the local GOM does not have an instance 1234 i as id = rmc.args["fooNode"] // this works i = 1234 n as noderef of class foo = i // this fails because the local GOM does not have an instance 1234
How do triggers work?
The period dictates the rate at which the trigger polls in seconds. The frequency of HeroEngine's event loop imposes an absolute limit to the period of a trigger as the trigger can not fire faster than the event pump for triggers (this is in practice 7-20ms).
For the sake of efficiency, it is best to have triggers polling no more often than is needed for them to perform properly. It should be exceedingly rare for a trigger to have a period of less than 0.25 seconds and generally 1 second is sufficient.
The volume of the trigger factors into the frequency the trigger requires to function properly, because of the way triggers track nodes in their volume and produce events based on changes to that tracking, a very narrow trigger may not produce any events for nodes passing through its volume because they are never inside the volume during the trigger's period.
Achieving "instantaneous" firing for trigger enter would make triggers significantly less efficient than the current design where you can choose to set the period to something that is appropriate to the volume and importance of whatever the trigger does.
How do I update my characters stats upon entering an area, or when another character enters the area?
The $ACCOUNT system node is where you would want to add additional code to your game specific override.
The specific methods you want to look for are:
- _EnteredArea - For sending information to clients other than that of the account logging in, _EnteredArea with its game specific overrides HE_PreEnteredArea and HE_PostEnteredArea is likely where you should hook in your calls to update their cache with information for the account logging in.
- _ClientReady - For sending information to the client logging into the area, you can not remote call it prior to the _ClientReady with its game specific overrides HE_PreClientReady and HE_PostClientReady for updating the client logging in with all of the cache information you need.
How would I go about displaying text on the screen?
Two basic options:
- Create a simplified GUI to handle the display
- Use the debugging capabilities of HeroBlade
Create a simplified GUI to handle the display
Create a GUI panel with X labels, ideally you are already caching the information on the client in some local data structure to which a script or your labels themselves listen (ala the observer pattern). A generic implementation of the observer pattern is included as a part of clean engine and is detailed hereObserver pattern. Personally, I would create a class GUIStatisticsPanel (archetype GUIControl which inherits GUIControl) in which you would implement the methods for updating the various GUI elements with data based on events raised. Use the debugging capabilities of HeroBlade
Another option, extremely quick and dirty version, would use the debugging facilities of HeroBlade to display the values see Debugging HSL#Real-Time Monitoring of Field Values .28Under Construction.29. This would require only that you make a remote call from the server to call the appropriate external function to update the built-in debugging panel with a new value.
How do I implement a selection mechanism?
Before you can select stuff, you need to have some kind of system that knows what things are selectable. You probably don't want every tree in a forest selectable for example. Once you have some sort of system that keeps track of that sort of thing, you will almost certainly want to layer on one or more features:
- a /command to make some object selectable
- capability of using the /command from the library's command interface to make things selectable automatically when added from the library
- automatically setting up players and creatures to be selectable
Telling the Client
Once you have something that knows about selectable things, you need to write the communication portion of the system to notify the client so it knows those things are selectable. You might do this during login to an area, or write something more complex based on proximity so that you do not tell the client about things it may never need to know are selectable.
Some sort of caching mechanism on the client is always a good idea for these types of systems.
For Hero's Journey, the highlighting is accomplished in the HJCursorClassMethods script which is a game specific override script for the CURSOR system node. It accomplishes this by (for nodes that support the properties) changing the Ambient and Diffuse lighting for the node.
Mouse Events on a Selectable Node
If you still have the HE_Mouse input layer active, then calls are being made to the INPUT system node. If you have implemented your own mouse input layer, then the INPUT system node is only called if you implemented calls to it to notify it about the various mouse events. Using your client cache, you can check whether or node is selectable or not and then do whatever you want about the click event.
What does $DEBUG do?
The $DEBUG system node is part of the Debug System. In essence, this is a system area that is kept up to receive debug messaging. There are no "channels" per se. Rather, each message is specified as being for a particular "system" which is just an arbitrary string name. All systems send their messaging on the Debug channel to chat. Users can (un)subscribe to systems by name, using the /debug command.
Why are we not seeing anything on the LogonProcess Channel?
This is probably because nothing has sent messaging for that system. For the system nodes involved in the logon process, we felt it was probably something people would want to toggle on and off dynamically (which #defines in HSL are not well suited to supporting). So the $CONFIGURATIONS system node was created, to serve as an interface to determine whether or not a licensee wanted the debugging for a particular system to be enabled or not. Like all system nodes, you create a game-specific class (MY_CONFIGURATIONS), GLOM it onto the prototype, and implement the override method (in this particular case it is HE_CoreScriptDebug). Then you can do whatever logic you want, such as to determine if you want to return true or false to tell whether or not the specified system (string name) should have its debugging turned on. If you wanted, you could even create a /command that modified the system node's instantiation to toggle things on/off on the fly.
What does the wildcard (*) state value do?
* is a wildcard value. Anytime you set a state to a new value (A->B) the following transitions are performed.
This allows you define transitions that always play when A transitions to any other value, A transitions to the specific value B, or whenever any value transitions to B. For each of those transitions (if defined), their action list (if any) is performed. This code is executed from the _StateObjectClassMethods script in the _SetStateValue method.
What are Properties and can I add new ones?
"Properties" which are displayed in the "Properties Panel" are special citizens in that they have underlying C++ code backing them, as opposed to fields you create using the DOM editor which do not have corresponding C++ backing. Consequently, you can not just add a field to the _GR2WritableInstanceProperties class without also adding C++ code supporting the addition and have it function as you intend. This is something easily handled assuming a source code license, but currently not possible without the source code.
What are Action marks?
The basic functionality of Action marks is a part of HeroEngine, what they do, how they do it, and what sort of GUI is presented to the user is entirely game specific code.
Action marks exist as a type of "helper" object/node that may be used in conjunction with animations and other game specific code to perform tasks that require "relatively" precise navigation/animation to get the right "look". What an action mark does is entirely game specific, as such the icons for HJ are HJs icons and even how our action marks function is very game specific (and to be perfectly honest in dire need of a refactor to take better advantage of features added to the engine after the prototype usage action mark usage was implemented).
The existing action mark types (other than GENERIC) may or may not be useful to your game because their visualizers are dependent upon the use of our C++ character controller. It is likely we will refactor the C++ implementation of Action Marks to better support extension and visualization needs for licensees. For now, any "new" type of action mark you want to make would use the GENERIC type and use the action mark tag field to store a string representing the "new" type.
The general logic flow for our action marks:
Why is _CharacterSystemClassMethods._SetCharacterName() called whenever a character's name changes?
The call to _CharacterSystemClassMethods._SetCharacterName() happens as a result of the _characterAppearance node's character archetype transmitting the new value for the node's name field (including when the character first enters the client's sphere of knowledge). This occurs for both characters and for npcs.
It should be noted that the _characterAppearance's name may have little to nothing to do with the character's actual name which is stored in the database and is cached locally on the _playerCharacter node's name field. The HSL clean engine implementation of the CCS sets both the _characterAppearance and _playerCharacter's name fields when it uses the external function UpdateCharacterName (which notifies the database of the name change) to set a character's name during character creation.
Consequently, the name passed to _setCharacterName is more properly thought of as the "visible name" reflected by the _characterAppearance node. This name is not currently cached for by any clean engine system. With the understanding that the value specified in the callback is a visibleCharacterName, I would be inclined to GLOM a (BWCHARACTERNAME) class onto the HBNode of for character containing a field visibleCharacterName in the game specific override method HE_SetCharacterName.
If you want the actual character name (as stored by the database), then you need to make a design decision if the visibleCharacterName and the actual character name could ever be different for your game. If they could, then you would need to work up some separate mechanic to request/transmit the character's actual name to the client and cache it in whatever way is appropriate for your needs.
Why are NodeRefs not automatically set to NONE when the node to which they refer no longer exists?
Given the following code:
foo as LookupList indexed by String of NodeRef bar as NodeRef = CreateNodeFromClass("someClass") foo["bar"] = bar DestroyNode(bar) println(foo["bar"]) // prints 4611686018471882963; uhoh… baz as NodeRef = foo["bar"] // Exception ConversionException: Node id  not found
The Exception "ConversionException: Node id  not found" occurs.
This particular behavior is both known and by design for all fields that use the noderef type. Taking into consideration several factors:
* Overhead/Speed * Expected Behavior
Please note, you CAN compare the value you stored against NONE.
if foo["bar"] = None // I destroyed it println("The node(" + foo["bar"] + ") stored at bar no longer exists or is not loaded in the local GOM.") return . // do stuff with foo["bar"] foo["bar"].DoCoolStuff()
We do not keep track of all noderefs that reference a particular node, the benefits for doing so did not seem to justify the overhead such a feature would require. Additionally, it could be potentially expensive to track down all the references and update them to NONE.
As experience programmers, it probably makes sense to us that it might automatically reset the value to None. However, HSL was designed with the novice programmer in mind.
For a novice programmer it is not intuitive when a value I store mysteriously changes. If the value is set to None, it leaves the novice no way to determine that it once had a value and no information about what was there. It is pretty easy to understand that the "object" might no longer exist and needs to be validated, but mysteriously changing values tends to befuddle.
Consequently, noderefs from a novice programmer's perspective appear to function much like IDs even though we know there are fundamental advantages to using the noderef instead of ID (and some disadvantages). Novice programmers are then taught to validate the noderef before they attempt to use it just like they would need to evaluate against NONE when they set a noderef to some ID.
Reasons a noderef might evaluate to NONE:
- Node Destruction
- Node not present in the current local GOM
Is there anything in HSL analogous to the STL multimap, an associative container that can have multiple identical keys that point to different values?
The short answer is not precisely, but you could effectively implement one in HSL.
The closest you would get to that in HSL would be something like: lookuplist indexed by string of list of string with a utility script (or methods if it is a field on a node) to handle the underlying mechanics or using the association engine if you are working with nodes.
Either way, you could write functions or methods that handled the add/remove/insert/list types of operations so that the user need not care how the data is actually structured.
How do I sort a list?
letters as list of string add back "c" to letters add back "b" to letters add back "a" to letters sort( letters ) // will sort the list alphebetically a,b,c
How does the event system work?
The Event System is as you note is more geared towards broadcast style events. As we have moved forward, we have gravitated more and more to using the ObsSubject/Listeners in conjunction with system nodes for system level events.
Those methods are a part of a generic implementation of the Observer Pattern, the basic classes involved are the ObsSubject and ObsListener classes, and are available for nodes that inherit obsSubject. This is unfortunately not yet documented, but some of this may strike a bell from the script training we did. The intended usage for this is very broad, essentially any node may inherit or have GLOMmed on the ObsSubject class which includes the behaviors necessary to send events to listeners, (un)subscribe listeners and generic utility functions such as listing all listeners.
"Events" for the ObsSubject/Listeners are node based and as such may be anything you need them to be, most commonly events are based on the parent class EventObject that has a few fields that are commonly useful. What type of node you pass for the event is up to you. High frequency events may wish to implement a singleton event object that they reuse, changing only the data. By default, event nodes are "owned" by the pattern and are destroyed after notification is completed (unless your event object is base_hard_associated to something in which case the event object is not destroyed.) this helps ensure your event nodes are not leaking memory.
ObsListeners act as proxies for an observer and come in a variety of types that know "how" to tell their observer that an event has occured. By default clean engine supports listeners that know how to call a shared function in a particular script, node listeners that do their notification to the "EventRaised" method on the node ( a shared function in the class method scripts, or a shared function in a script stored in the "script" field ), and versions on the server that act as proxies for clientside nodes/scripts. ObsListeners implement a method to determine whether or not their observer cares about any given event from the subject, so listeners can listen for very specific events or all events of a type. By overriding the generic methods of the ObsListener class and/or its children you can customize the behavior to do pretty much anything.
ObsSubject/Listener classes are used extensively in Hero's Journey for GUIs and most of the major systems. Clean Engine GUIs including the Area State Editor also make use of them, off the top of my head I am not sure how many clean engine systems use them.
Creating a subject
A) GLOM the obsSubject class onto a node
B) Inherit the obsSubject class as a parent of one of the classes from which your node is constructed
Of the two, I view inheriting as the "best practice" though there are situations where GLOMming might be preferred.
Creating a listener
By default there are 4 (on the server) methods you may call via the subject node to create a listener: createNodeListener, createScriptListener, createClientNodeListener and createClientScriptListener. Each one returns a noderef of class ObsListener that acts as a proxy object for a script or node, but does not automatically add it to the subject's list of listeners.
You may GLOM additional classes onto the listener to store any data you need the listener to have. You are not limited to only these 4 listener types, you may create your own listeners by creating a class that has the ObsListener as a parent and then overriding the parent class's methods as needed. Specialized subjects may implement their own createListener methods to create listener objects that are uniquely suited to a particular subject's events.
// The boolean in this method call indicates whether the listener should be persisted or not, // persisted listeners only work for persisted subjects var listener = mySubject.CreateNodeListener( nodeToNotifyID, false )
In some cases, you may not need to create a listener at all but rather the node you want to have notified may itself inherit directly from the obsListener. Eliminating one layer of indirection.
Adding a listener to a subject
Adding a listener is a simple method call.
mySubject.addListener( listener )
Sending an event
Sending an event to a subjects listeners requires a node that represents the event. This node can either be of a generic type such as a node created from the "eventObject" class or specific to a particular subject.
eObject as noderef of class EventObject = createNodeFromClass( "eventObject" ) eObject.eventType = "playerarrived" eObject.eventCausedById = account eObject.eventAffectsID = account // the default subject does a PUSH, you could implement subjects that work based on PULL mySubject.setChanged() mySubject.notifyListeners( eObject )
ObsListeners receive events in their method:
method EventRaised( obs as NodeRef of Class ObsSubject, data as NodeRef )
Where data is a noderef to whatever node you passed to the subject's notifyListeners method. In the case of obsListenerScript listeners, the listener will then pass the event on to the script specified in a shared function EventRaisedNotify. NodeListeners try to call the EventRaised method on the node from which they are acting as a proxy, then the class method scripts at the shared function EventRaisedNotify, and lastly a shared function in the script contained in the .script field.
Important ObsListener methods
The methods you are most likely to override for a listener class of your own are:
The isAlreadyListening method is used to prevent a system/node from registering multiple times for a given subject (though in some situations this is ok), depending on your listener class you may need to override this to evaluate additional information you store in custom fields to determine whether or not there is already a listener.
The eventRaised method determines what a listener does when it receives notification of an event happening. What happens can be anything you can script.
The isListenerInterested method allows you to determine whether or not a listener cares about a particular type of event (based on its class, the data, or anything else).
The destroyListener method is commonly overriden to prevent the obsSubject from automatically cleaning up the listener when the listener unsubscribes from the subject.
The Event System
While the system is perfectly functional for broadcasting events, it is my recommendation that a more targeted usage with the ObsSubject implemenation is likely more ideal. For the more generic system events, I recommend creating a system node which is a subject that generates events particular to a given system. This increases the odds that listeners are interested in the events that are being raised by a given subject and will ultimately be the more optimal option from a processing perspective.
For example, the states system node could inherit from ObsSubject and raise any events related to the states system.
How do I copy field values from one node to another?
1) Write an external function in C++ that accepts two nodes and a string class name which performs the copy operation.
2) Utilize the fieldCollection Pseudofield
There is the capability to access a field on a node by string using the pseudofield "fieldCollection", which is used as it if were a lookuplist indexed by fieldname of value.
b as boolean = myNode.fieldCollection["booleanFieldName"]
The limitation to this functionality is that you currently are required to have a properly cast variable to assign or to accept assignment from the fieldCollection mechanic. This means that currently it is not possible assign the value of one fieldCollection to another field collection:
// not currently possible myNode.fieldCollection[f] = otherNode.fieldCollection[f]
Currently, if you know you are working with only the basic field types you can create a when/is statement that based on the field type properly casts the value and assigns it to the other node.
fieldType as string = GetFieldType( f ) when tolower( fieldType ) is "boolean" b as boolean = source.fieldCollection[f] destination.fieldCollection[b] = b .
If the source node, is composed of classes you want copied to the destination, then marshalNode will provide the necessary capability of copying the data from one to the other. It is perhaps heavier that is absolutely necessary, but if this is not a particularly high frequency call it may not matter in the slightest.
var marshalString = MarshalNode( source ) UnmarshalNode( marshalString, destination, true )
3a) Write a marshalling utility that uses marshalnodeappendfield to only marshal fields within some class
Once you implement the method (I have not tested this so use with caution) you simply use UnmarshalNode with the marshal string the method assembles.
method marshalClassForNode( source as noderef, className as string ) as string // assemble a list of the classes (inherited) that have fields that will need to be marshalled classes as list of string marshalUtils:FindParentClasses( className, classes ) fields as list of string foreach c in classes // getClassFields does not include inherited fields var localFields = GetClassFields(classname) // add to master list if not a duplicate foreach lF in localFields if not strUtils:ListOfStringHasString( fields, lf ) add back lf to fields . . . // assemble a marshal string containing only the fields in the class marshalString as string foreach f in fields MarshalAppendField( marshalString, source, f ) . return marshalString .
4) Implement a copy Method in the class method script that handles the copy explicitly
method copyFooToDestination( dest as noderef ) dest.fieldCollection["fieldA"] = me.fieldA dest.fieldCollection["fieldB"] = me.fieldB // etc
How do I read player input?
There are two major places you can intercept player input:
- In the character controller itself in its _onBehave() method
- In the command layer(s) that handle your movement input (Command_layer)
Depending on your needs, its possible you will need to use a combination of both of these. Basically, when you press "W" the input system sends a command to the input layer scripts (because the keybindings file defines a mapping). The default movement layer translates the "forward" command that is sent into behave commands to send to your character. The character controller receives the behave commands and interprets them into a series of states for the controller.
How can I temporarily disable controls?
Using the input system (Input_system) you can activate and deactivate command layers.
How do I detect when a player is moving?
With replication and the Advanced Customizable Character controller, you can detect that the server controller has received an update and determine whether or not the character should be considered to be "moving" based on whatever criteria you have for your game.
How do I modify the Fade Out(Render Panel/Mesh) default value?
Objects LOD (including fading) based on a calculation of their screenspace volume and the LODFactor property exposed by Asset Instances.
There are a number of external functions for working with the graphics options exposed by the render panel. No switches/sliders exposed by the render panel are exposed to HSL as some are intended for debugging or editing uses.
external function EnumerateGraphicsOptionSwitches() as List of String external function SetGraphicsOptionSwitch(option as String, value as Boolean) external function GetGraphicsOptionSwitch(option as String) as Boolean external function EnumerateGraphicsOptionValues() as List of String external function SetGraphicsOptionValue(option as String, value as Float) external function GetGraphicsOptionValue(option as String) as Float
How do I know the order in which a group of characters enters a volume in space (A.K.A. who won the race)?
The order of events is non-deterministic. Simultaneity is an illusion in MMOs by the time you factor in: internet latency, latency built into the simulation of npc movement vs frequency of movement updates from a player client, asynchronous processing of events, processing for a trigger is at most once per server frame, asynchronous processing between server processes, variable server frame length based on HSL runtime, the only approximately accurate nature of synchronized time, the maximum resolution for time in HeroEngine (approximately 15 ms), and a host of other factors its pretty much impossible to know what happened first.
How do I handle persistence in HSL?
The tutorial purposefully did not address persistence as the topic would have added considerable complexity to what is supposed to be a quick bite-sized taste of manipulating the game environment with HSL.
The client GOM is by definition non persistent as it only exists as long as the client application is running. When you create a node in the client GOM (either via /cnfc CLI command or via external function in HSL), the node's existence is tied to the client running. While it is possible to write some types of information to the Local Repository Cache, it would not be generally appropriate for this type of application. At the Server level, a persistent node is saved to the DataStore (by default that is of course an Oracle database) based on the write strategies defined at the field definition level. Changes to area geometry are only persisted if they are made in the Edit Instance.
The introduction of Asset Instances (i.e. things that are a part of the Area Geometry by virtue of adding an instance from the Asset Library or the Asset Panel) happens by virtue of reading the area definition from the Repository (.DAT files) and then applying any pending Edits, all of which is handled by underlying C++ which deals with a specific set of fields. Modifications of the Asset Instance to extend it with additional game specific classes/behaviors (such as the one you add during the tutorial) are not included in the area definition files so even if you add a class to the server instance it does not inform the client when it loads the area.
Ultimately, you could communicate the necessary information via remote procedure calls to the client during connection to a particular area or save file(s) to the Repository that store lists of all "clickable" instances. However, a more realistic example of how you would implement game objects with the capabilities demonstrated in the tutorial is demonstrated in the Replication Tutorial (Replication_Tutorial). This however is a much more advanced topic and involves a lot of additional code and understanding of the fundamental mechanics of the engine.