This title is pretty lengthy due to the specificity of the problem.
The issue stems from SLG Movement’s failure to generate a path. This is rooted in the wrong Board placement of the Chess (which, in this case, is meant to refer to the mover), which, specifically, is having two or more instances of the Chess in the same Layout.
This happens when the Board’s Action:Create chess is executed on the same Layout where the Chess mover exists. This instantiates another copy of Chess. I think this fine, but when SLGMovement’s Action:Get moving path is called, and if the Chess is not unambiguously picked (SOL), then it will use the first Chess, which is not really assigned to the Board.
I noticed this particularly in Rex’s example capx where he puts the ‘resources’ — eg exemplar Sprites — into a separate Layout.
I think having a ‘resources’ Layout is good practice, however, this should only be used for Sprites intended for instancing, as those directly manipulated will not be visible in the current Layout if they are manually set in the ‘resources’ one.
Basic stuff. The main idea is to create the Tiles as the TMX is being imported. Rex explains it in the Scirra forums.
Rex’s example for putting tiles into Board based on TMX data.
Layering up Tiles and Chess
Tiles are the elements used for determining the Board’s logical positions. Chess, on the other hand, are elements above the Tiles. As mentioned in Rex’s docs, Tiles are those residing in Z=0, while Chess are those that reside in Z>0. The distinction cropped up because I wanted to generate Edges based on a custom property of a tile.
But what was happening was this: I was generating Tiles on the board using Board’s Action:Create tile. This not only instantiates the Sprite, but registers its presence as a Tile on Board. Now, TMX Importer was generating other Tiles from other layers, so it was overwriting some of the Tiles that I had just placed. When it came to query the Tiles in order to determine where an Edge should appear, it was always referring to the latest Tile that was put in that logical position, and some of those Tiles had no edge requirement.
The solution was to put the TMX tiles as Chess entities on top of each other on the Board in order to distinguish them from one another. It was a matter, then, to decide which TMX tiles would be Board Tiles, and which would be Chess. I decided that the base Floor layer — for now, at any rate — should be the Tiles, and every other TMX tile would be Chess.
As part of my solution, I thought it would be intuitive if the Board’s z-ordering matches the layer ordering in Tiled (rather than C2’s layers matching tiled, since C2’s layers have a different function from Tiled layers anyway). So what I did was to initialise a C2 Dictionary to contain the TMX layer names along with their layer indices (eg tmx_layer[“Floor”] = 0). Then when a Tile is being generated, it knowing what TMX layer it belongs to (eg tmx.LayerName), that is looked up against the Dictionary, and placed in the appropriate z-axis.
So Tiles can be some generic Sprite. But to make it more efficient, the Tiles should represent all ground areas, even those not necessarily impassible, but at the least the areas which are of programmatic interest.
Tiled’s Objects and Board’s Logical Positions
Here are some tricky bits regarding TMX’s Objects (note that Objects, capitalised, pertains to the Object entity in the Tiled prog). In the first place, querying a TMX’s Object’s ObjectX/Y parameters inside Condition:On each object, will give you the coordinates as it is written down in the TMX file (observable in the Tiled Editor). The problem with this is that that the coordinates is not a Logical position, nor is it a screen-space (aka ‘Physical position’) position. It is actually a position in orthogonal-space!
There are no Expressions in Rex’s Board plugin that computes this, because the computation is dependent on the projection, which is reasonable. Instead of maintaining another Board for a trivial lookup, I just implemented my own using functions.
There are some interesting points here. First, in Tiled Objects, the X/Y values use pixel values, and they use the Map’s Tile Height as the normalising factor. Because this is an isometric map, the width and height of the tiles are not the same and because the tiles have indeed been transformed, you have to ask what is the resulting pixel X/Y value that defines the end of one tile and the beginning of another. It turns out that the tiles use the tile height:
Using C2 Tile Object is limited to orthogonal, both in terms of doing the design in the C2 layout, or importing TMX files through the Tilebar functionality in C2. In other words, native C2 is a no-go for isometric levels.
Rex’s TMX Importer V2 Workflows
I turned to TMX Importer v2.x (tmxiv2) by Rex, which is a runtime importer and supports a much wider aspect of Tiled’s functionality. By ‘runtime’ this means that the map is not loaded into the C2 editor, and can’t be seen until the level is being previewed. This a bit of disadvantage for me, as I am used to editing things in C2. But I have been getting predictable results and I think, with enough practice, the workflow will be seamless.
The TMX Importer uses Sprite as though it were a tilesheet. It uses the animation frame of the C2 Sprite as a tile id.
Get TMX via AJAX
The standard way to retrieve text (ASCII) files from external source is to use the C2 AJAX object. In Rex’s own TMX examples, he uses pasted strings into the input. Either way, the workflow to do this is as follows:
Using TMX Importer v2.x
Once the AJAX object reads the TMX file, it will run Trigger:On completed. Here we take AJAX.LastData as the content of the TMX file, and feed that into tmxiv2. There are two main methods of importing: creating tiles immediately, or retrieving a tile array.
Creating tiles (Action:Create tiles) is simpler, but it obeys a pre-defined behaviour which requires you to create C2 layers to match the TMX layers. This becomes a hindrance when you consider sorting Sprites. As I will touch on later about Rex’s ZSorter plugin, we need many of the Sprites in one layer. And so Action:Create tiles will not do that for us.
So the other way of doing it is to use Action:Retrieve tile array, which kicks off a procedure that starts getting the tile information. Note that the TMXXMLParser is used because a TMX file is an XML.
As each tile is being retrieved, it triggers Trigger:On each cell tile, which is the place to configure the tile as they are being placed. There is a caveat however. Although a Sprite instance is being created in Trigger:On each cell tile, there seems to be a problem accessing at least some instance information and thus some actions are not possible in that trigger. In the picture above, I had to reposition the tiles after they were placed by tmxiv2. I had tried to do it in Trigger:On each cell tile but the reference to tiles.ImagePointX/Y were not being read at all. While this may be a bug, I decided to use a trigger variable that is switched when the retrieving is finished in order to act upon the tiles instances from a different scope.
In order to incorporate z-sorting, which is the sorting of Sprites based on their Y-position, the Sprites must reside in the same layer as the player character, and those moving elements expected to move in front or behind of other Sprites.
The first obstacle to this is the ZSorter’s use of the Origin imagepoint as a basis for determining the Y-position. This is not customisable, so I developed a workaround.
Place the Origin imagepoint in respect to how ZSorter references this position for sorting.
Create a new imagepoint called ‘center’. This is the imagepoint used by TMX Importer for placement. This image point resides at the point where the center of the logical position is.
So what happens is that when TMX Importer first imports the tiles, it will default to using the Origin imagepoint, which will likely cause many of the tiles to be offset. We remedy this by using the vector difference between the Origin imagepoint and the desired center imagepoint and applying that difference to the tile
The Board plugin has several important dependencies that must be put in the project depending on what functionality is desired. Some of the following notes are not necessarily in Rex’s docs, which sometimes can be sparse in detail, though his examples explain things very clearly. So, in effect, the following is a distillation of the things learned in the docs and as it relates to the examples.
Before anything, the Board must be setup, which, at its most primitive, just the Board object, which defines the logical positions of the tiles.
The Board is responsible for creating the virtual logical grid (ie tile coordinates with logical positions). It specifies its dimensions.
This is a plugin that allows sprites/tiles to be placed on the Board as they currently are positioned in the editor. This is a convenience feature that is more useful if I was editing my level in C2. However, Tiled is my editor and will place tiles procedurally through other means.
This was an improvement that I personally suggested to Rex which is an upgrade from the original squareTx plugin. (Currently, ProjectionTx is not available at his site). The purpose of these ‘Tx’ plugins is to display the Board’s logical positions as a particular projection. Rex has kept these two aspects separate, as it is easier to visualise a ‘top-down’ Board as the basis for computation, and a ‘projection’ as a basis for defining how that Board is going to be visually represented.
When a call to Board’s Action:Create tile is made it uses ProjectionTx to place it in screenspace. It also seems that all other calls that involve visual representation, Board uses ProjectionTx to translate it.
The image to the right is ProjectionTx’s property panel. VectorU represent the direction of the U (or X or left-right) of the tile (if you imagine looking top-down) and VectorV represent the other axis. In other words, ProjectionTx is asking what is the screenspace direction of the X and Y axis of the tile. In an isometric projection, 32×16 pixels sprite, UX=+32, UY=-16, VX=-32, VY=-16. Explanation: X axis of the tile right and down: right for 32 pixels (+32), and down 16 pixels (-16); the Y axis of the tiles goes left and down, hence -32 (left) and -16 (down).
The Edge plugin works in conjunction with Board and movement. Edge objects exist between tiles, and the Edge object itself is queried if a particular edge exists between two tiles. This might contrast with the instinctive notion that we query the Board if certain Edges exist in its logical positions. After all, it is the Board’s turf. However, the way it works is that Edge objects keep track of themselves and where they exist between tiles.
When Edges are created, a Sprite is used to visually represent the Edge. It is thus rotated perpendicularly to the VectorU or VectorV of any given tile depending on the side of the tile the Edge is being created on. In isometric tiles, this produces a wrong result, since the orientation has a isometric ‘skewing’, and thus the angles must be set manually.
Movement is more involved than Board setup. It involves at least two plugins working in conjunction with each other, and involves a third if pathfinding is needed. For my needs, I need all three.
SLGMovement is the plugin that is responsible for pathfinding. Its responsibility is querying the board for the best possible path, and taking that information and feeding it into the InstGroup plugin (explained later). Thus, SLGMovement has two dependencies at all times: Board, and InstGroup.
Implicitly, it will use the first Board and InstGroup object in the scene.
However, a specific Board and InstGroup can be specified through its Action:Setup call.
The plugin computes for ‘cost’, which is the concept of how many ‘points’ it takes to travel to a tile. In the same vein, it can also prevent movement from one tile to another (for example when tile is impassable).
Edge objects can be optionally queried in cases where movement between two tiles cannot occur at a particular direction between each other (though it can occur if the path goes around the Edge).
Through the example SLGMovement works this way: the path to follow is computed beforehand. This means that SLGMovement calls the On cost function repeatedly as it makes its way to query which tiles it can use to the desired destination. When by setting the cost as SLGMovement travels to the destination using its pathfinding algorithm, it can determine the best way. If SLGMovement.BLOCKING is encoutered, this means that this particular ‘leg’ of the path is not possible, and SLGMovement will try to find another way around.
Get moving path and InstGroup
The heart of SLGMovement is its Get moving path function, which calls the pathfinder. It has a few parameters:
As the captions explain, the ‘moving cost function’ is the heart of the pathfinding querying. It’s here where a valid path is traced based on the moving points and the results of the ‘moving cost function’.
The weird thing about this setup, however, is in the InstGroup, which is an implicit dependency. You need InstGroup to make SLGMovement work because there is no other way to store the resulting path.
Once the path has been stored, the next part is the actual movement, which is handled by GridMove.
Once SLGMovement has stored the path in InstGroup, GridMove is used to access the path. The principle is to start the chain of calls to GridMove by ‘popping’ the first element that’s stored in InstGroup. The first ‘pop’ and move occurs in the ‘Mouse on click’ or ‘Touch’ trigger itself. When the first node is ‘popped’ it is SOL’d, and thus a call to ‘GridMove move to tile‘ will yield the SOL’d tile as the target.
Then on Condition:On GridMove reach target, another instance is ‘popped’ out of the InstGroup. As the caption above describes, the ‘popping’ of the the instance is a GridMove condition. The condition will return a False if there are no more elements to ‘pop’, and in the image above, Function “GetMoveableTile” is run after the path has been reached.
Note that the ‘popping’ occurs at the head of the InstGroup array. This denotes that the sorting of the path nodes where the first element is always the next waypoint down the line.
Connection to Board
Normally, I would expect a explicit connection from GridMove to the Board, and hence, access to ProjectionTx in order to find the screen position of the logical coordinates. But as I look at it, the GridMove is attached as a behaviour of any given ‘chess’ (ie a movable element on the Board), and a ‘chess’ instance can only be in one Board anyway. So it seems that GridMove is able to trace back the Sprite’s association to the Board it was originally created in.
So, these are the following procedures that comprise isometric tile-based movement.
Board plugin as the basis for providing logical coordinates for positioning, and populating its own tiles with content through procedure.
(Optional) LayoutToBoard plugin as a convenience feature to populate Board with ‘chess’ data based on how the objects themselves are placed in the C2 editor in relation to the established Board.
Edge plugin as another element that is can be used in conjunction with SLGMovement to determine pathfinding. Though Edge is not technically related to Board, determining Edges is best done during the setup (though it can be changed any time).
ProjectionTx plugin as a visual transformation of the Board into a desired projection. In this case I’m concentrating on an isometric (or possibly trimetric) projection.
SLGMovement plugin as the pathfinding engine. It is connected to two other plugins: Board and InstGroup. By default, it will use the first object of each kind, so there’s no need to setup SLGMovement unless you need to. SLGMovement computes the cost of movement from one tile to another in the Board, and in this ‘cost function’ you can influence the pathfinding (eg blocking tiles). It then puts the resulting path into a named group in InstGroup.
InstGroup plugin as the container and actionable condition that works in conjunction with GridMove to ‘daisy-chain’ the stored waypoints. InstGroup allows ‘popping’ of instances in the array (ie group) which give the effect of moving to the next waypoint which is then fed into GridMove
GridMove behaviour as the function that moves the Sprite. GridMove is implicitly connected to the Board that the Sprite had been generated in using Board’s Action:Create tile method.
Knowledgebase for creating a Construct 2 isometric game.