Tag Archives: tmx

Thoughts on displaying adventure text

Adventure text can be long, and should be variable as much as possible.

In the RND test, I’ve formulated an initial design that has the following aspects.

Terms (TextTerms)

The use of terms mean that a keyword is used as a topic. So if I used the term ‘missionsuccess’, then I could define that the displayed text is ‘Mission successful! Congratulations!’

Furthermore, I can use a TMX entity, for example an npc with the name ‘letigus’, and use that as my term. And thus define ‘letigus’ as ‘Hi, my name is Letigus. What can I do you for?’

Also, through the use of terms, I can encapsulate a variations into one term. This is the syntax which I use to implement this. This is written in a separate text file which read by the AJAX object and then put into a Dictionary using the CSV2Dictionary plugin by Rex.

Each term is defined by at least 2 lines. One line is a declaration of how many variations there are. And the succeeding lines are the content of the term.

<term>|<default index>/<maximum index>
<term>.<index>.<text type>.<duration>|<text>

The first line’s <term> is the declaration of the term’s name. The default index refers to which index it should start with. The maximum index is expected maximum variations of the term.

The second line’s <term> signifies this is the term being used. The <index> is defining this particular term’s index. <text type> can be anything that identifies the kind of text it is supposed to be. For example, the text could refer to a person speaking, or it could refer to a ‘narrator’, or it could refer to a description of something. This <text type> is used to colour the text according.y The <duration> is the number of seconds that the text will hold. If 0, it will hold indefinitely, which is useful in cases, for example, if a player is killed, and a ‘defeat’ term is used. Then there is a bar ‘|’ and the <text> follows afterwards.

Note that in the text file, there are no newlines, so the use of ‘\n’ is just a marker for a C2 text replacement for an actual C2 newline character.

To recap, here’s an example:

look_r1|1/2
look_r1.1.desc.0|The place is a smelly hole.\nAnd a sight for sore eyes.
look_r1.2.desc.0|You don't know how anyone could live here.

So at the start, ‘look_r1’ is defined as having 2 variations, and it starts at the first index.

Then look_r1’s two texts are defined, each specifying their own index, and type, and duration, and finally, the text.

Displaying the text

The display of the text is then offloaded to another function (DisplayText) which sets the duration and colour based on the data it receives from the Term itself.

For example, if the type is an NPC speaking, the DisplayText function will wrap the text in quotation marks, and maybe give it a different colour.

External text file, map-specific

As I said before, the AJAX and CSV2Dictionary plugins are used to transfer an external text file’s contents to a dictionary, and following the format above, this is relatively trivial.

CSV2Dictionary requires a pair separation by a specified delimiter. I chose the default (comma) and used the  bar ‘|’ as a separator within the tokens.

In the RND test, there was no distinction between map-centric adventure text and more generic/global text. However, this should be implemented as it makes most sense.

Advertisements

Thoughts on accomps

Accomps is ‘accomplishments’ and I’ve separated map-related accomps (m_accomps) with global accomps (g_accomps). Global accomps persist, while m_accomps are re-populated  when a map loads.

This separation may not be necessary, but for the meantime, there it is.

accomps have the key as the accomplishment, and the value is either 0 or 1. 0 if not yet accomplished (default value), and 1 if it is.

In the RND test, the accomp related to exiting an area was triggered 1 every time the player was on top of the designated area (ie current_trigger variable). But when  the current_trigger="", it triggered (using TOWT) a reset to 0. So as long as the player was not on the trigger, the accomps will always revert to 0.

On the map logic script, the ‘success’ of a map is checked whether all (or some) of the accomps have been set to 1. Because it is map specific, anything can be written here.

In the TMX side, there are some inputs that need to be done. First, the map’s accomps are written in the Map’s custom properties called ‘accomps’. Eg, accomps=talk:npc1,kill:guard1,remain:exit

It is done in the TMX because it is only relevant for this map. There are other data that could be offloaded here.

In this case, when player talks to an NPC, this interaction is marked as ‘talk’, and thus the trigger is sent as ‘talk’. If player talks to ‘npc1′ then this is sent to npc1. Now in the TOWT trigger for f_trigger_receiver.f_name==’npc1’, it is responsible for writing the proper key to the accomps. The syntax is free-form so care must be taken.

In the RND test, the killing of an enemy was not broadcast, but this is a good example of implementing such a broadcast (eg BroadcastTrigger(“killed”,f_trigger_receiver.uid)) within the main logic.

Then in the main logic, a trigger receiver is actually waiting to receive the news of death. When it does, it takes care of its own key in the accomps: accomps["kill:guard1"] = 1.

 

Workflow: Setting up the Board for movement

Synopsis

This post is going to try to be the most definitive guide to setting up the Board system from scratch for the purposes of movement. This means it will hit the following requirements.

Requirements

  1. Maps using Tiled/TMX via TMX Importer V2
  2. Board setup based on TMX reading
  3. SLG Movement for pathfinding

Mandatory objects

  1. AJAX, for reading TMX files
  2. Board
  3. Browser, for logging
  4. Function2M (or any function plugin)
  5. Keyboard, escaping movement
  6. InstanceGroup, for storing path information from SLG
  7. SLG Movement, for tile pathfinding
  8. SquareTX
  9. TMX Importer V2
  10. TMX-XML Parser

Mandatory behaviours

  1. GridMove, for the movers on the grid.

Setup

tmx_1

  1. AJAX calls the tmx to be loaded, which gets it as a string.Then the trigger AJAX:On completed is called.
  2. SquareTx’s position offset is set to (16,16); ie the position offset is the physical coordinates of LogicXY (0,0). In an orthographic setup (which this event sheet screencap is based), the value of 16 refers to the offset so that the center of the tile would be moved inside the layout, and the top-left corner of the upper-left most tile will be aligned squarely at the layout’s (0,0) coordinates.
    1. In an isometric position the map height plays a part. 
  3. SLG is configured to use the Board as its Board, and an InstanceGroup (ig).
  4. The mover’s GridMove behaviour is configured to use a particular InstanceGroup for its data.
  5. Make sure that any object that is to be instantiated using the TMX process is destroyed. This makes sure that during the instantiation the proper object is being referenced.
  6. When AJAX completes reading of the tmx, it will trigger its On completed event.
  7. Use the TMX Importer V2 (tmx) to import AJAX.LastData using the XML parser. This populates the tmx object.
  8. Then set the SquareTX’s cell width and height to correspond with the tmx.
  9. Set the Board’s width and height (logical entries) to correspond to the size of the map in the tmx.
  10. Then initiate the tile retrieval.

TMX Data Retrieval

I’ve not yet documented the timings of Objects vs Tile retrieval, so I’m not making any dependence on timings.

Tiles are retrieved first before Objects.

Movable areas

When creating areas for movement, I prefer to create a Tiled layer for movable areas and leave tiles blank where it’s not possible to move on, rather than tagging tiles impassable, so I don’t need to check this during the cost function.

On each tile cell

  1. Use Board:Create tile to instantiate the tiles and place them on the board.
    1. Board: Add chess could also be used, but this is more confusing because it only places a logical ‘marker’, and does not instantiate it.
  2. Configure the frame of the tile (ie id of the tileset).
    1. Note that some it’s not always the case that you can configure the tiles after they’ve been created, for example, re-positioning tiles after instantiation didn’t seem to be reliable or possible. But it seems that instance variables are ok.
    2. TMX tile properties are set as necessary

On each object

Same sort of thing as On each tile cell.

  1. Create using Board:Create chess.
  2. Note: pay attention to the Board z-index as well as the C2 layer.
  3. Configure non-Board related stuff as needed.
  4. Note OXY2LXY, which is the ‘Orthogonal to Logical coordinate function’. To repeat a past post, the TMX Object’s position is recorded in orthogonal coordinates, and the Board or SquareTX object have no convenience features to translate those values to isometric. The OXY2LXY function is this translation:

This completes the TMX retrieval.


Initiating the move

The first thing to consider is the first call to move.

In this case it is a LMB on a tile.

  • Note ig:Clean group. This removes all previous path entries (in this case it is “path” referring to the waypoint nodes)
  • The main command is: slg: Get moving path start from <mover> to tile/chess <tile> with moving points to slg.INFINITY and cost to <cost_function> then put result to group <instance_group_name>
    • <mover> refers to the chess that is already on the Board.
    • <tile> refers to the tiles on Z=0 on the Board, which is the basis for moving.
      • Presumably (haven’t checked), if <tile> is at a specific Z index on the Board, then SLG will consider that Z index and pathfind on that level only. But what if the <tile> is at Z=2, for example?
    • <cost_function> is the cost function of SLG which determines the resulting path. We can also call this a path function.
    • <instance_group_name> is the group name inside the InstanceGroup object which stores the UIDs of the pathfinding nodes.
  • Then on the condition that the GridMove is not moving the mover, we pop the first waypoint, which is the first waypointand this is SOL’d as the tile object.
  • With this SOL, direct GridMove to move to that tile.
  • This the initial movement phase.

Cost function (moving path function)

Before dealing with the continuation of the move, we must define the cost function of SLG on the mouse click.

The cost function, also called moving path function, is called by SLG when a moving path is required.

  • The basic definition of a cost function to make set the the return cost to 1.
  • Returning a cost of SLG.BLOCKING will make this tile impassable
  • Use slg.TileUID as the reference to the tile being queried for pathfinding.
  • If the map was generated with blank areas, then there’s no need to check against those, as they won’t be even be considered for pathfinding.

Continuing the move

Once the move has been initiated, then continue to move as long as there are nodes in the InstanceGroup for paths.

  • The continuation of the move is on the GridMove:On reach target trigger, which is triggered when GridMove moves on top of each tile as stored in the InstanceGroup.
  • Use the condition InstanceGroup:Pop one instance <tiles> from group <instance_group_name> in order to determine if it has popped the last one. If it has then GridMove is bypassed.

Movable area function

The movable area function may or may not be used in Citizen 2401, but this is a good time to document this function.

SLG has 2 ‘cost’ functions. One is the movable path, and the other is the movable area.

Focus on the SLG call.

Just like movable path, movable area’s search pattern is to move out from a logical coordinate.

What the events above are trying to do is to generate a list of tiles which the AI can move to that are not LOS’d by the player. The LOS of the tile is determined by another function which switches the los instance variable accordingly, so that only this variable is checked.

The movable area cost function itself (‘p evade area’) does not check for the LOS state, and the reason is described here. Simply put, because of the movable area search ‘creep’ may get blocked by LOS’d tiles, only the tiles are tested on a distance basis (ie movement cost as defined in the SLG:Get moving area)

Then, a filter function is applied on top of the results of the movable area function in order to get rid of those tiles that is LOS’d.


Stopping, changing paths

To stop, simply clear the InstanceGroup path group. This will give GridMove no waypoints to go when GridMove:On reach target is triggered.

When trying to LMB on a tile while still moving, simply clean the Instance path group.

This has the effect of generating a new path while making sure the GridMove still goes to the last assigned waypoint.

Workflow: Animated Tiles

Dedicated animated tiles

The syntax to activate animated tiles is in the ‘vars’ (variant) variable of each tile. Again, to recap, the Tiles’ ‘vars’ property denote the any number of states that the Tile can be in. For every state specified in the ‘vars’ property, there is a corresponding data related to the graphic that will be shown when the Tile (of that Layer) reaches that state.

Example:

vars=open*:a,close:a:15

Note * and ‘a’ and ’15’.

The * denotes that it is the default state of the Tile.

The ‘a’ denotes that instead of using another Tile ID for this state, use an animated variant.

The ’15’ denotes an alternate Tile ID which is an extra level of control in order to find the animated Sprite

Ok, so how does it evaluate which animated Sprite to use?

Through the use of Nicknames to reliably instantiate the tileset Family (ts_f) each animated Tile variant is given a unique name. The syntax template is:

<tilesheet_name>_a_<tile_id>_<state>

What this means is this: let’s say we have the above ‘vars’ property in the Tile from the ‘objects’ tilesheet, it’s Tile ID is 10. For a give state, let’s say ‘open’, the system will look for a nickname called:

objects_a_10_open

If this ts_f exists then it will be picked. This ts_f is a dedicated animated sprite for that tilesheet/tile ID/state combination.

Now, as another example, if the Layer’s Tile state becomes ‘close’, the ‘vars’ property above specifies:

close:a:15

In this case, it will look for a nickname called:

objects_a_15_close

No ‘nominal’ state

As an aside topic, this is about whether tile states (ie TcoDict) should have a ‘nominal’ (or default) state. This was apparently not a good idea, since it is logically ambiguous. A default of any state make it impossible to know what is the converse value of it.

At any rate, I’ve made all TcoDict unambiguously have a state, and this is inherited in the C2 import of the TMX.

The behaviour before was that for each TCO, it reads the ‘layers’ property and there initialises the state. This could be tedious in the future. And since it is possible to indicate the state of a Sprite/Tile by visually looking at it (eg open door, closed box), I will do it by reading the Tile’s ‘vars’ and looking for the -1 directive. Whatever state it is, it will be written as the default state of the Tile in the TcoDict.

Workflow: Changing Tiles

Overview and purpose

I’ve worked out a method of changing tiles at run-time using the same kind of Object ‘tagging’ system I’ve outlined in previous posts (eg Storage) using the same reasoning: the need to identify entities uniquely.

The purpose of changing tiles is to indicate a state of a Tile/Chess, and visually represent the change.

For example, a door’s opened or closed state, or a floor’s damaged or otherwise modified state.

Implementation

tco_explainer

Three elements are needed for this implementation that I call Tile Change Object (TCO).

  1. TCO Object (defined in the TMX)
  2. TCO Sprite (created in C2 through TMX loading)
  3. TCO Dictionary (created in C2 in conjunction with TCO Sprite)
  4. Tiles ‘vars’ property
A TCO Object in the TMX is defined by a 'tco' type. Additionally, it needs a custom property called 'layers' to define the extent of its control over which Tile is on which Layer.
A TCO Object in the TMX is defined by a ‘tco’ type. Additionally, it needs a custom property called ‘layers’ to define the extent of its control over which Tile is on which Layer.

TCO Object

The TCO Object defined in the TMX is any Object with type ‘tco’. It requires a custom property called ‘layers’ which is a comma-separated string defining a list of Layers that is meant to be changed. by this TCO. For example:

layers=Walls,Floor

Again, the TCO Object only marks the tile as changeable. The configuration of how it changes is defined in the Tile itself.

TCO Sprite

The TCO Sprite is a Chess object that resides in its own z-layer in the Board (“Tco”). The Sprite is the effective marker of the TCO in the Board.

TCO Dictionary (TcoDict)

When a TCO Sprite is created on the Board, it creates a TCO Dictionary (TcoDict). This TcoDict’s keys is thus added using the TCO Object’s ‘layers’ property values:

TcoDict["Floor"] = ""
TcoDict["Walls"] = ""

Note that the value is empty, and this means it is in its nominal state.

Tiles ‘vars’ Property

The 'vars' property define the possible states that the Tile could be in, and the Tile ID to use if in that state. A value of -1 means to use the 'self' Tile ID, and implicitly means that the nominal state of the Tile is as defined.
The ‘vars’ property define the possible states that the Tile could be in, and the Tile ID to use if in that state. A value of -1 means to use the ‘self’ Tile ID, and implicitly means that the nominal state of the Tile is as defined.

(Edit: the ‘vars’ property syntax is always in a state of flux so this part has been edited a few times)

The Tiles in the TMX which are expected to change need a property called ‘vars’. This ‘vars’ property define a state, and the Tile ID it’s supposed to use when the Tile and Layer is switched to that state. Example of a ‘vars’ value:

vars=open:42,closed*:-1,broken:5,burning:a,burninglow:a:2

The ‘vars’ line is tokenised by commas ‘,’, which separate it into what we’ll call ‘subvars’

open:42
closed*:-1
broken:5
burning:a
burninglow:a:2

Then it is further tokenised by colon ‘:’.

The first token is the possible state that this Tile could be in. If that state has a *, it is considered to be the default state of the Tile. This is important in the initial creation of the map, to initialise the state of each Tile.

The second token is the Tile ID (of the same Tilesheet) that represents that state. If -1 is specified, it means to use the original Tile ID that the Tile originally began with.

If ‘a’ is specified in the second token, then that means that an animated Sprite is intended.

If ‘a’ was specified in the second token, then the third token can also be specified to denote to use another Tile’s animated Sprite.

For reference this is the syntax template for a subvar:

<state>[*]:<Tile_ID>|a[:<Alt_Tile_ID>]

Refer to the Animated Tiles post on how this is used.

Workflow: Persistent data using a custom Storage entity

Storage

As a continuation from the post Workflow: Room loading with TMX, I’ve implemented one persistent data type called Storage to be able to demonstrate a workflow that will keep data between TMX loads.

Storage is an entity placed in Tiled (an Object). It is named s#. When the TMX is read, it instantiates a ‘storage’ Sprite onto the Board on its own ‘Storage’ z-layer.

It is then initialised: the Tiled property ‘name’ is copied to the C2 instance, and the room (my term for the currently processing TMX file) is also populated thus. (Unrelated, there is also a property called ‘content’ which is the initialising value for the Storage’s contents.)

After the Storage Sprite is initialised, it is effectively a TMX representation in C2. Now, I associate a Dictionary (named ‘StorageDict’) with this Storage Sprite. StorageDict has a ‘name’ and ‘room’ instance variable which are the two mandatory associations to match itself up with the Storage Sprite.

When a Storage Sprite is initialised it searches for its StorageDict partner using ‘name’ and ‘room’ instance variables. If it find it, then nothing needs to happen further — just making sure that there is actually a container for that storage. If it doesn’t find it, then it makes one. The only connection between a Storage Sprite and StorageDict is the ‘name’ and ‘room’ variables/properties.

When the TMX is unloaded and another reloaded, the StorageDict remain in memory. Loading and saving C2 will also keep the data. In other words, the association is ‘soft’.

EDIT: I’ve decided to directly relate the storage Sprite by adding an instance variable. This connection is always made every scene load, but it does save me from having to re-evaluate which StorageDict a storage Sprite is referring to.

Where to from here?

This demonstrate the use of a Dictionary to maintain C2 instance data. It is in itself the data and up this point there is no requirement to visually represent changes (eg tile id change based on a new value).

But I suppose this is where the next challenge is: changes in tile usage depending on saved data.

Workflow: Room loading with TMX

InstGroup

The call to load another TMX is easy because I’ve designed the events to be generic. However, all C2 instances generated by the TMX, which have been placed on the board, need to be deleted before another one could take its place.

I’ve used InstGroup in this way: I store all the different types of Object Instances in the InstGroup, such as Edges, Tiles, Chess, Portals, etc, into a InstGroup group called ‘board’.

Then there is a nice convenience feature in InstGroup that simply says ‘Destroy instances’ and will allow you to pick which group. This is super-clean, though I noticed that the UID numbers of destroyed instances (eg Tiles) do not get re-used, so the numbers keep on going up.

Portals and player_start

In Tiled, I’ve used Object Layers to define portals and start positions. The player_start entity is only defined by its name ‘player_start’.  It may be a good idea to make this more generic, but I’ll leave that for later.

Portals, on the other hand, I expect to be placed a lot more, so this object has the type property called ‘portal’. The name of the portal refers to its identifier in the context of the room. For now, the syntax goes like p#, where # is simply the number.

In Tiled, the Portal entity has one custom property called ‘destination’, and the syntax for that is r#.p#. The r# refers to the room number, which is the same as the TMX file (eg r1.tmx). Later, in C2, this ‘destination’ property is tokenised to give out the TMX file to be opened, and the portal to go to.

In C2, the Portal exists in a z-layer called ‘Portal’.  To clarify, a z-layer refers the Board’s z-index. During TMX loading, when a Portal Object is encountered, it places a Portal Sprite into that ‘Portal’ z-layer and is tested later when there is an attempt to use the Portal (Cell is occupied)

I’ve used a global variable to track the intended Portal, since it doesn’t seem that there are any advantages to doing this through some parameter in the TMX loading.

So, the TMX is loaded by the tokenised ‘destination’, the portal, also tokenised, is stored as a global variable; then the TMX goes through its processing, and when processing Object Layers, sees the portal that the player must start in, and places the player there.

Persistence

Some thoughts re save/load functionality: saving and loading the TMX object will yield only the data that had already been previously read, but the TMX data itself could not be changed by C2 because the TMX Importer doesn’t have methods to do that.

So what’s happening here is that the TMX is loaded, and I transfer pertinent information from TMX into C2 through positioning, instance variables, etc. So what I have is a functionally static TMX as the basis for a scene/room, but loading/saving of this TMX file means nothing in this context.

So how to go about persistence?

If we load the TMX every time there is a portal movement, instances are deleted, and then recreated and applied the changes post-load, very much like Maya’s referenceEdits.

Or, could we use multiple C2 Layouts and jump between them? However, C2 destroys objects when moving to another Layout unless that object is flagged as global.

So, do we store state data for specific things before room transfer and then apply them back on when loading? The issue here is knowing which entity held the data to begin with. For example, if a Chess sits as a container (of data), like a chest, then when it is recreated again by room loading, how do we identify that Chess uniquely again? I consider tile ids to be volatile.

I think the only way to uniquely identify something is by using Objects.