Tag Archives: tiled

Slidewalk TMX and C2 system

The Slidewalk system, taken from 2400AD is implemented by using several components in Tiled, and hooking them up in C2.

Tiled/TMX Component

The Slidewalk is comprised of several objects which are the children of an ObjectGroup; the ObjectGroup, in effect, is a single Slidewalk entity.

Components of a Slidewalk

What identifies an existence of Slidewalk is not the ObjectGroup, but the individual Objects which must be named starting with the prefix sw. Each Object component of the Slidewalk contains the name of the Slidewalk it belongs to. The name of the Slidewalk is the name of the ObjectGroup.

A Slidewalk must always have a path. A path is a Tiled polyline. It must be named swpath. The path will later define the waypoints of the Slidewalk. The swpath Object must also contain an attribute called speed. This is the speed value of this Slidewalk.

A Slidewalk must have at least one entry-only point. This point is defined by using an Object (parented under the Slidewalk in question). It must be named swin.

A Slidewalk must have at least one end-only point. This point must be named swend.

A Slidewalk may optionally have an exit point. This exit point may also be a potential entry point. Every point, whether it is an entry, exit, or end point, must begin with sw. Then use the keyword in to indicate this is an entry point, and the keyword out to indicate it is an exit point; these keywords may be used in the same point, such as swinout.

The exit and the end points must also contain a custom property called exitdir. This is a GridMove direction that specifies which direction the player will move towards when deciding the exit, in order to get off the movement effect of the Slidewalk.

C2 Implementation

In C2 the first step is to get the swpathObject to mark the waypoints of the MTiles. In the same procedure, they are marked with the name of the Slidewalk they belong to. The waypoint sequence index is internally based on how Tiled writes it, which is sequential anyway, so the C2 loop iterator does this conveniently.

As the MTiles are being tagged as waypoints, they are being added into the InstGroup with a group named after the Slidewalk’s name, which uniquely identifies these series of MTiles for SLG and GridMove. They are added in the order they are looped, so the sequence is still preserved at this point.

MTiles are then further marked with their entry/exit/end values, as well as the custom properties as inputted in Tiled.

When the player moves on top of the Slidewalk, the On GridMove reach target trigger will check whether the player is on an entry point. If it is, a function called CreatePathFromIG is called whereby it takes the Slidewalk InstGroup waypoints and transfers those waypoints to the player’s own InstGroup moving path. CreatePathFromIG does something more, though: it considers the possibility of multiple entry points; it finds the MTile that the player has entered from, and starts retrieving MTile waypoints from that point until the end.

 

 

Advertisements

MTile/GTile edge definition syntax

Though this syntax was developed many months ago, I had forgotten to document this.

Overview

This edge definition refers to the edges that exists in any GTile tile/sprite. This obviously means that edge definition is a Tiled property of the GTile, which is then passed on in C2 to generate MTile edges at runtime.

Note that a GTile refers to a graphics tile which is a bigger-sized tile. An MTile refers to a movement tile which is smaller and forms the grid of possible tiles to move to. Every GTile is subdivided into equal square MTiles.

Edge definition

Below is an image representing one GTile, and the MTile IDs within it:

For every GTile, there can exist any number of edges. Using this image as an example:

That sprite is mapped out as:

Here, we introduce the edge syntax used to define those edges.

<mtile_id>:<gridmove_direction>[,<mtile_id>:<gridmove_direction>...]

Where mtile_id is the id of the MTile that needs an edge definition (because it is neighbouring a GTile edge), and gridmove_direction is the direction where the edge lies on that MTile specified in the GridMove format.

In the sprite above, the edge definition is:

1:0,5:0,8:3,9:3,10:3,11:3

That is, the MTile ID 1 and 5 have a edges at Direction 0 (east), IDs 8, 9, 10, and 11 have edge at Direction 3 (north). Note that it’s possible to have defined other MTiles, such as:

6:1,7:1

…to replace 10:3, 11:3. It doesn’t matter, as long as the same edge is not specified twice.

Tiled

Note that the edge definition is inputted in Tiled in its tilesheet editor. Currently, there are two Tile properties associated with edge: edge and edge4. edge was the original use, which subdivided each GTile into 2×2 MTiles. edge4 subdivided it further (4×4) giving it a maximum of 16 MTiles per GTile (this depicted in the image above).

In the C2 project, only one type is used and can be interchanged or modified depending on final design choices.

Workflow: Using AnimationLoader, dynamic tilesets

As usual, Rex has done it again. Or rather, he’d already done it.

The look development of the game is what I’ve always been concerned about. I’m creating tiles, binning them, replacing them. The tile management in Tiled is a bit too simple for my needs.

First came the issue of volatile tile IDs. Then the inability of re-arranging the tiles. So what I’ve done now is to offload all the actual organisation of data to 3d and C2 workflows. What this means is that I no longer have to use tile IDs to reference.

This is an outline of what I’m thinking of designing (and have implemented it for the most part at the end).

Renders

3d renders, or wherever the tile renders may come from are rendered directly to the C2 folder’s /Files directory. They have the following name convention.

_<tileset_name>_<tileset_id>.png

This collapses the tilset name and tile id into one name. These are then put into a flat hierarchy in the /Files directory.

C2 animation name

Back in C2, I create a single tile sprite. The content of this sprite can be empty because the idea here is that the image will be dynamically loaded in.

This is the gtiles sprite (GBoard tiles), and contains (I think) all of the tilesets used in the game. Now the tilesets are stored in C2’s animation blocks. So in the gtiles sprite, I create all the tileset names that should exist:

proto_gnd
proto_walls
proto_canal
..etc

This refers to the naming convention of the .png described above.

C2 animation frames

After the animation blocks/tilesets have been created, then I note down how many frames each tileset should have. I create the exact number of frames for them though the frames are blank. If I don’t I will get errors in the Browser console, but it won’t stop the execution of the game.

AnimationLoader

Rex’s AnimationLoader handles the loading of the renders in the /Files folder by going through every animation block in the gtiles sprite, and then through every frame that it has. I’ve written events that concats it properly, adds the underscores to conform with the convention, and it loads the images into those frames.

Tiled

This is the important bit.

In Tiled, when I load in the tiles into the tileset, I must reference the png files in the /Files folder, because when the TMX is read, the image source from the tile is also read. This image source TMX attribute is the string that is parsed in order to determine which tileset this belongs to, and what the tile ID is. So, in effect, by the naming convention alone, regardless of which tileset the tiles belong to in Tiled, the fact that the file contains the tileset and id, that’s enough to make a connection to C2. In this way, it doesn’t matter how I arrange my tiles in Tiled because I only use the name of the png for the correspondence.

Collisions

Collisions are often defined in the C2 image editor, and the information is stored in the caproj. However, when dynamically loading sprites, the corresponding collisions must also be dynamically loaded.

I’m currently implementing a solution to this which involves writing out .col files (collision files) that is partnered with their respective images. This much in the same vein as .imagepoint files.

Collision polygons are defined in Photoshop using paths. A ‘Work Path’ must be created, and a jsx to save that path is run. This outputs a .col file alongside the .png file that is being edited.

Back in C2, I’m currently modifying Rex’s AnimationLoader to take in this .col file implicitly, by reading the file, and then injecting the information directly into the poly_pnts array of the Sprite object.

 

Workflow: Test project RnD 2017 03 25

I’ve been testing a lot of concepts (some old, some new) with a test project and this post is about what I’ve learned, and what else needs to be explored.

CSVToDictionary and AJAX

It’s easier to maintain a separate text file for populating lookup dicts. Use the AJAX object to read the text and then CSVToDictionary to populate the dict. Remove the double-quote marks when using a text file. This makes it easier to read.

Newlines in text files

When extracting text using AJAX, newlines might be necessary, but escape characters do not seem to be recognised. Therefore, I ended up using escape characters, but had to process it (using search-replace) during extraction.

Containers

Containers have been extremely useful especially in terms of debugging messages. For every object I need to debug, a debug Text object is created, and querying the instance of the object will always point to the same objects of the container. No additional picking is necessary. This is probably the most important aspect of my testing.

Enumerations

There are no real enumerations in C2, but simply assigning a constant number to a recognisable variable name is good enough. For example, in the case where the z-layer of a logical position needs to be identified by keyword, I use Z_TILE=0, Z_WALLS=1, etc.

AI

Although a topic unto itself, the main takeaway from doing AI, is how triggers are setup in Tiled and how it’s set up in C2 to respond to triggers.

There are area triggers, which are set up in Tiled. These are positional, and in the test project, they included a ‘facing’ property, which meant that the trigger is fired only when the player is facing a certain direction. The trigger’s name is the string that will end up being called by C2. I opted to use the ‘name’ attribute in Tiled instead of the relegating it to a property because it’s clearer to see the object name in the Tiled viewport.

Some triggers are set up in C2, especially other kinds of interactions. For example, talking to an NPC will yield a trigger specific to the interaction.

The C2 trigger itself is tied to a particular entity, whether it is another NPC, or some other object. That object is responsible for keeping track of the global trigger calls, and what is relevant to itself. For example, if a certain trigger is called 3 times, the object must keep track that it has heard those 3 triggers, and act accordingly. As such, two variables are meant to store AI-specific data: scriptmem, scriptmem_float. The scriptmem variable is meant for strings, and the scriptmem_float is for float value. For example, scriptmem_float was used a generic timer (for waiting). On the other hand, scriptmem was used to store how many times the AI has heard a specific trigger by checking and appending keywords onto the string.

Another important thing about AI is the switch between scripted AI and ‘nominal’ AI. Nominal AI is one that is already pre-programmed in C2 where if there are no scripts directing the AI, it will follow a certain logic (which also depends on the type of AI it is). Two things needed to happen. First the AI needed to know which AI it was allowed to switch to, and this was put in a C2 variable called ai. For example, one agent was assigned ai=script,see. This allowed the agent to switch to ‘script’ mode, but also allow state changes to occur when the C2 ‘see player’ trigger was fired. There was a ‘hear player’ trigger that existed, but because this was not included in the variable, the agent did not respond to hearing, only seeing, and only triggers involving scripts. This ai variable assignment is first done in Tiled, and then propagated to the agent during TMX load.

In addition to the ai variable, the agent had to be put into an FSM state called “script” when it is in scripted AI mode, which allows the system to distinguish which part of the AI sequence it is in. The C2 events which constitute the AI for that agent must consider other FSM states, like “idle”, which is often the ending state after a move.

AI is a bigger topic and I will delve into it more when needed.

Grouping

I find, more and more, that groups are quite useful not only in organising and commenting, but allows simpler conditional actions to be done in-game. The only example I have is the deactivation of user input if a particular state is on-going. This makes it trivial to block input rather than having check conditions of state all through the user input event.

SLG movement cost

SLG movement cost functions can actually be quite simple. At first I thought it needed to accommodate many aspects, but in the end, despite the relatively complex requirement of the test AI, pathfinding, at most, needed only to query the LOS status of a tile. Impassability was bypassed by excluding impassable tiles from the MBoard, making pathfinding simpler and, I think, faster.

It’s also probably best to name SLG movement cost functions with the following convention: <char> <purpose> path. Eg: “npc evade path”, or “npc attack path”, whereby in the “npc evade path” the NPC avoids LOS, and “npc attack path” does not avoid LOS at all.

Orthogonal and Isometric measurements

I’ve researched and learned a lot of about how to transfer orthogonal measurements to isometric values. The positional values were simple enough, but the real progress was in computing angles.

Here is a list of important considerations when dealing with isometric stuff:

  • Converting a C2 object angle (orthogonal space) to an isometric angle (OrthoAngle2IsoAngle). This is used to draw a line in isometric view if that line’s angle was the same as in orthogonal view.
  • Converting an angle depicted in isometric view to orthogonal space (IsoAngle2OrthoAngle). This is used to determine what an angle would like when viewed from top-down. So when you measure the angle between two points, it’s not truly the angle when viewed in orthogonal space because the isometric view is skewing things. IsoAngle2OrthoAngle allows the reverse computation so that, for example, LOS could be determined for a particular point.
  • Converting orthogonal XY positions to logical XY (OXY2LXY). There is no Board function that allows this mainly because this is a peculiarity of the way Tiled positions objects of the Board. The positioning of objects is written in orthgonal space, but when Rex’s SquareTx projection is set to isometric, then all measurements become isometric. Thus this function is needed for this lack.
  • Movement Board and Graphics Board versions of OXY2LXY. This is required because SquareTx for each Board is different, and thus the logical positions will yield a different location.
  • Computing isometric distance to orthogonal distance. This measures two points in isometric space and gives out the distance as though you were looking from above. This is useful in determining the distance between foreground and background objects. This uses a SquareTx as a point of reference for the width/height ratio, but can use the MBoard or GBoard, because they are assumed to have the same ratio.
  • Computing snap angle (Angle2SnapeAngle). Looks at the object’s angle, and finds the nearest angle to snap to (assuming 8 directions). This is required so that the proper animation is set.
  • Converting MBoard logical positions to GBoard logical positions and vice-versa (MLXY2GLXY, GLXY2MLXY). This is very important as it is able to relate the MBoard to the GBoard. Because the MBoard has smaller cell sizes, querying the logical positions of the MBoard using bigger GBoard logical positions will always yield the top-left cell of the MBoard.
  • Convert GridMove direction to C2 angle (GetGridMoveDirection). The GridMove values are quite different. This function converts it for use with other things, like animation, or other function related to facing, which use the C2 angle, or snap angle.

LOS

The last part of this post is about LOS. I’ve already wrote about some aspects of this. But the main path of the research lay in the following:

  • A Line-of-sight behaviour is applied to the player.
  • The player’s facing angle is taken as orthogonal.
  • An LOS field-of-view is defined (eg 90 degrees) for the player.
  • At a given angle (facing_direction), left-side and right-side fov lines are drawn based on the defined fov (90 degrees). Note that these lines are virtually drawn orthogonally.
  • The left and right lines are then converted to isometric angles.
  • Because the left and right lines have been transformed, the difference between these two angles have changed. This new difference is the new LOS field-of-view.
  • The center between these two lines is the LOS center line. The player’s LOS is rotated towards the center.
  • With the new LOS field-of-view, and a new center, this corresponds to an isometric LOS based off a 90-degree LOS when viewed orthogonally.
  • The facing_direction mentioned above bears special mention. When a player clicks on tile in-game, he is actually picking with a view that he is viewing it in isometric view. Therefore, the facing_direction is an isometric angle, which must be converted to an orthogonal angle. It is only then that the left and right fov lines can be properly oriented, because they, in their turn, will be converted back to isometric after the computation is done.

What needs to be explored

ZSorting seems to take a lot of cpu time (~50%), and I’m wondering whether there is a way I can optimise this. So far, the best solution I’ve come up with is to use On GridMove as a condition for sorting. But I think the most ideal way is to localise the sorting around the areas where movement is taking place.

 

Workflow: Tiled’s tile IDs not to be referenced in C2

Tiled’s tile IDs are quite volatile. Ordinarily, each tileset’s first tile should be 0-index, but unfortunately, this doesn’t seem to be guaranteed and I’m not sure if it should.

In this forum post Bjorn himself suggested not to use tile IDs as a reference, and instead use tile properties to make a harder connection, which is what I’ve done: created an ‘id’ parameter and and just retrieved it from there.

The only issue here is that each tile ID must be manually entered, but this better than figuring out how to make tile IDs behave.


To make it solid, write a tool that reads the C2 caproj, reads the Sprite object, stores the image source for each frame of a Sprite: a text lookup to associate a filename with an ID.

For example, the lookup can read:

proto_walls walls_block.0000.png 0
proto_walls walls_block_cross.0000.png 1
proto_walls walls_block_ne.0000.png 2

Where the first token is the tileset name, followed by the image filename, followed by the animation frame, which is the ID.

Then the tool is run again on the Tiled-side. Going through each line of the lookup, searches for the tileset name, locates the filename (maybe ignores the relative path), then sets the ‘id’ property to the stored value.

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.

Placing Image Objects on the Board

In this post, under the section Tiled’s Objects and Board’s Logical Positions, point out that the positions for Tiled’s Objects  are set in orthogonal space, as though we were viewing things from top-down. There are no convenience features in the Board plugin that will translate this, so, like previously, I had to come up with a computation that will translate orthogonal coordinates to the desired isometric view.

orthoxy2isoxy
The function inside C2.

The gist of it is this: In orthogonal, X means X and Y means Y. To get to a point, it is p=(x,y).

In isometric space, a movement along X is perceived as a movement in both X and Y, ie the point goes from upper-left to lower-right. Again, for every X movement in orthogonal space, there is a X and Y movement in isometric space. Therefore, you have to get the directions in isometric space for X and Y.

When you get the X and Y axis vectors, you multiple your unit movement with those vectors.

The uox and uoy are the values I refer to as the isometric unit vector for X and Y axes, respectively. We don’t use uox or uoy directly, because there are actually 2 components. In uox and uoy, there are the X and Y components:

uox_x # how much iso x movement from ortho x
uox_y # how much iso y movement from ortho x 
uoy_x # how much iso x movement from ortho y
uoy_y # how much iso y movement from ortho y

Then it’s important to know the unit value of the orthogonal values; we want to get a ratio of movement: how far between fixed tile sizes are we trying to pinpoint?

So the orthogonal X and Y are divided by the tile’s height, which is done because Tiled actually does it this way (the post linked above explains this).

ux = in_x/TileH
uy = in_y/TileH

Then the final bit is to add up how much movement did a ortho X affect iso’s X and Y, and how much did ortho Y affect iso’s X and Y.

out_x=(ux * ox_x) + (uy * oy_x)
out_y=(ux * ox_y) + (uy * oy_y)

This allows me to place Objects as though they were tiles on the Board. Of course, the placing of Tiles/Chess components on the Board is still being done using the Board’s functions.