Tag Archives: Isometric

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.

 

Advertisements

Converting angles to isometric angles

I don’t think ‘isometric angles’ is even a term, but I’m referring to how an angle should appear like in isometric view.

The image below describes the situation.

Problem

Given an  angle (75deg and 45deg depicted in red arrows above), what is computed angle if the circle was compressed by half (ie an isometric view). The small ellipse depict a circle in isometric view.

The yellow arrows depict the scaling effect of the point on the big white circle and is projected to touch the isometric circle.

The green arrows represent the vectors whose angles I am computing.

Solution

I must apologise to any potential researcher that this solution is entirely homemade and there might be more elegant solutions out there.

  • With a given angle, determine the normalised length of the Adjacent side A. This is done by cos(angle).
  • The length of the A enables the measurement of the Opposite side O.
  • O is computed by multiply A with the tan() ratio of the angle: O = tan(angle)*A
  • Now that O has been found out (the yellow arrow line depicted above), scale the length down as per the isometric projection. For simplicity, I’m scaling it by half (eg for 256×125 tile): O2=O/2
  • Then recompute the ratio of the known A and new O2: ratio=O2/A
  • The new ratio can be used to determine angle using atan(): new_angle=atan(ratio)
  • If angle reaches 90, then A has no length, and this corresponds with the nominal orthographic angle.
  • If angle reaches 180, then O has no length, and so the same thing.
  • If angle > 90 and < 180 the result of A=cos(angle) will be negative.  The same with tan(angle).
  • If angle > 180 and < 270 then A=cos(angle) will be negative still, but tan(angle) will be positive.
  • If  angle > 270 and < 360 then A=cos(angle) will be positive, and tan(angle) will be negative.
  • When result angle is reached, modify the value based on the quadrant of the original angle:
    • If cos(angle) is negative, and tan(angle) is negative, add 180 degrees. (Quadrant 2)
    • If cos(angle) is negative, and tan(angle) is positive, add 180 degrees. (Quadrant 3)
    • If cos(angle) is positive, and tan(angle) is negative, add 360 degrees. (Quadrant 4)
    • If cos(angle) is positive, and tan(angle) is positive, then keep result. (Quadrant 1)

C2 angle

The images above show 45 degrees as pointing NE. In C2, however, it’s pointing SE. But this orientation allowed me to understand what was going on.

C2 Implementation

Thus.

Working rather well.

To reverse the operation, to get the orthogonal angle from an isometric angle, reverse tile’s width/height in O2: O2=O/(SquareTx.Height/SquareTx.Width)

 

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.

Workflow: Tiled to C2 – TMX to Board

Creating tiles on the Board from TMX Importer

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

Using Rex's Function2M to convert Tiled orthogonal-type Object parameters to Logical positions.
Using Rex’s Function2M to convert Tiled orthogonal-type Object parameters to 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:

tiled_object_layer_positioning1
At (0,0), the ‘upper-left’ corner of the object is aligned with the top-left corner of the grid.
tiled_object_layer_positioning2
With the grid/tile width/height to be 256/149, moving the Object X=+149 pixels brings it up neatly to just the end of that tile. At +150 pixels, it will lie on the adjacent tile.