The Slidewalk system, taken from 2400AD is implemented by using several components in Tiled, and hooking them up in C2.
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 swpathObject 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.
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.
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.
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.
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.