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.

 

Advertisements

Startup C2 Objects

C2 is not that great with modularity or even copy-pasting across different projects. So here is a list of objects needed.

Global

  • AJAX
  • GBoard (Board) – tile graphic placement
  • MBoard (Board) – movement logic board
  • GSquareTx (SquareTx) – for GBoard
  • MSquareTx (SquareTx) – for MBoard
  • Gslg (SLGMovement) – for GBoard
  • Mslg (SLGMovement) – for MBoard
  • ig (InstanceGroup)
  • Gmask (LogicMask) – mask for use with GBoard
  • pmover (Sprite) – player mover
  • nmover (Sprite) – npc mover
  • gtoken (Sprite) – token for use with the GBoard to get logic position and other uses
  • tmx (TMX Importer V2)
  • tmxparser (TMX XML Parser)
  • CSV2Dictionary
  • CSV2Array
  • Audio
  • Browser – debugging
  • fn (Function2M)
  • Keyboard
  • Mouse
  • NWjs
  • ZSorter
  • Edges

Instance based

  • AText (Text) – adventure text, for narration, NPC, and player specific. Container-grouped with pmover, or nmover for picking purposes.

f_atexters – those that can display

Data

  • gridmove_dir_lookup (Array) – converts GridMove logical directions (0-7) to snap angles (0-315).
  • gridmove_dir_lookup_rev (Dictionary) – Converts snap angles to GridMove logical directions. Keys are snap angles, values are logical directions.
  • DirectionArray (Array) – an array of all possible snap angles. Size of 8, (0-315)
  • gridmove_seq_dir_lookup (Array) – remaps GridMove’s logical directions to a sequential, clockwise manner, so that starting from 0 (facing down right in screen space), each facing increment adds 1 to the facing id.
  • gridmove_seq_dir_cardinal (Dictionary) – Refering to the sequential directions above, this relates a cardinal direction to a direction integer. Eg “N”=0, “NE”=4.
  • adventure_text (Dictionary) – contains adventure text. This will be instantiated later on based on room.

Debug (Text)

  • info_p – for player
  • info_n – for npc
  • info_mt – for MBoard tiles
  • info_gt – for GBoard tiles
  • info_w – for walls

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.

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.

 

Thoughts on triggers

On the RND test, here are some thoughts on triggers.

Triggers are broadcast by a function. Triggers may have a targeted object/instance. In order to target any potential object, they’re put into a Family, which I’ll refer to as f_trigger_receiver (f_tr, for short).

There are 2 parts to triggers. The ‘main’ logic, and the ‘map’ logic. The main logic handles generic logic of triggers.

Main logic

f_trigger_receiver

The Family for all trigger receivers. Requires f_name, and f_receivedtrigger variables. f_name is the name of the entity which a trigger will use to refer to this instance. f_receivedtrigger is the string identifying the trigger that has been sent out.

On player GridMove reach target

Fired every time the player moves into a tile. This queries if a trigger area was stepped on.

Also, the player must have a current_trigger variable which keeps track of the trigger area it is on at any given time. This prevents re-triggering when the trigger area covers adjacent tiles. Also, this allows to find out if the player has stepped out of a trigger.

BroadcastTrigger

A function which handles the send off to f_trigger_receiver. It accepts a trigger_name, and a trigger_target. The trigger_name is the identifier of the trigger. The trigger_target is a comma-delimited string that identifies the objects/instances that the trigger will be sent to. The f_trigger_receiver family is used in order to go across different object types.

Other interactions

Any other interactions deemed worthy of a trigger only has to call the BroadcastTrigger function, and feed it an object that can accept a trigger.

The RND test, for example, had broadcast an NPC interaction generically by feeding it trigger_name="npctalk", trigger_target="npc1". Then the trigger was broadcast only on npc1 and processed accordingly.

There are no  ‘global’ triggers (ie triggers must always have a target). If a ‘global’-like trigger is needed, it might be better to use the player’s mover token as that, since it’s as global as you’re going to get.

Map logic

Map logic refers to the map/room-specific stuff.

Typically, the triggers for a particular room are stored in a separate event sheet (which I call scripts).

Time triggers

I put the time triggers in the map because it’s more specific to the map/mission. I still call BroadcastTrigger, but the trigger_name is specific to the map, of course.

Time triggers include ‘per-tick’ or any kind of time-related triggers.

Self-initialisation

Some instances need to init themselves before going into play. For example, a waypoint traveller needs to init the first waypoint index. This is done using the post_tmx boolean check, which is basically a switch that tells that the TMX has been completely read, and all objects have been created (and thus referenceable).

Other triggers and functions

Any other kind of triggers, whether they’re from FSM or TOWT, can be put in the map logic script. In the RND test, I’ve put in unique FSM states (eg “reachpath”) to put it in a special state so that the rest of AI can contextualise itself.

Map-specific functions are put here as well.

 

 

 

 

Workflow: ZSorter, sorting other imagepoints

In this post I write that there is no way to customise the behaviour of ZSorter to deal with sorting objects that have a different imagepoint as a sort reference. I was wrong. In fact it is possible to sort objects out a specific way using ZSorter’s On sorting function trigger.

Simple sort using a particular imagepoint rather than the default one.