C2 Sprite Manager

2017-02-23-08_02_32-c2-sprite-manager-b1-_-r__game_projects_citizen_demo_b_c2_c2-caproj

Overview

The C2 Sprite Manager (C2SpriteMan) was written by me, Lernie, and is a Python (exe wrapped) that views and modifies image files and their corresponding data as it relates in a .caproj (C2 project) file.

This project was originally done for LW (called LWC2), and for speed and flexibility, I migrated the code to Python and turned it to a Windows-based utility.

This tool revolves around a specific pipeline which involves mass creation of sprite assets from LW (or any 3D prog) and then conforms those renders to something that can be plugged into C2.

The tools works upon existing Sprites as they are defined in the .caproj file. This is due to the fact that ‘sid’ attributes in C2 might be better of be maintained by the host prog, so new objects are created only within C2, not externally.

The tool organises sprite graphics into ‘units’ (left panel) From ‘units,’ the individual sprites are displayed (right panel) where the name, its version number, frame duration, image-point file and crop-file and crop-image-point file validation, speed, looping and such parameters as found in C2 itself.

First, the frame number is determined by the actual number of frames found in the unit’s folder, and is used to debug if more or less frames were rendered than expected. There is an option to preview the unit animation using djv (hard-coded, so it djv must be installed). The frame rate is fed as an argument in djv so it mimicks what you would expect to see in C2.

The image-point, crop, crop-image-point files are another thing. First, the image-point is a special file generated in LW which contain normalised X and Y coordinates of the hotspot, and/or an image-point for a particular frame of the animation. It is formatted thus:

_IP_hotspot 0.1 0.2
_IP_gunpoint 0.5 0.12

Again, each frame of animation has a corresponding image-point file, and are named in a specific convention that makes their relationship apparent.

NULL.0001.png # rendered frame
NULL.0001.png.imagepoint # ascii imagepoint file

Note the use of the ‘NULL’ name. This is a constant name. Remember that these files are contained under the unit animation folder, so they are unique; there is no need to make them unique from other units as C2SpriteMan identifies the image based on the folder they are in, very much the same way C2 does it.

The next step is to crop the sprite, and ImageMagick (IM) is used as the tool for it. In this process, IM outputs a file that defines the cropping operation that it has done on the image. Since any number of sprites will most likely have a different cropping result from each other, this information is important because I can use it to modify the image-point file, which changes based on how much was cropped from its partner NULL file.

These cropped files are named as such:

CROP.0001.png
CROP.0001.png.imagepoint

These CROP files are used by C2SpriteMan as the actual usable sprites that is meant to be used in C2,

After cropping, the sprites are copied over to the C2 folder, which is defined as the /c2 subdirectory under the overall project folder. (Since I use many applications, I keep a main project trunk with major areas of delineation, depending on how much relative dependencies are needed for a particular use. For instance, LW, Maya, and C2 all reference relatively. But LW and Maya reference the same things, such as texture maps, cache, other shareable data. C2 references its own assets and files. So I kept the 3D side separate from C2)

The copy procedure transfer all renders to the C2 folder in strict accordance with how C2 expects them to be: file names are stripped down, and become the ###.png format, and are put in their own sprite folder. They were managed in 3D in the same way as C2 to make this process easier.

Once copied, all data can be ‘committed’ to the .caproj file. What ‘committing’ means is that any parameter change made to the sprites (eg changing Speed) will be updated in the .caproj file. This is a write operation on the .caproj, and a backup is made every time it is done. There is a drop-down menu on the lower-right that allows for rolling back to a previous version.

From the top procedure

I’ve just explained the process, but not in the sequence that it would otherwise be done. So this could be confusing reading for my future self. So this is how it’s done from the top-down.

Note that .caproj (folder-based projects) is necessary to use, for obvious reasons.

C2: Create placeholder Sprites

In C2, Sprite objects must be create beforehand. It is important to name the Sprites accordingly because this is essentially the unit name that need to correspond with the renders in 3D.

For purposes of illustration, let’s call our unit ‘hunter’.

Save the .caproj so that these modifications are reflected in the file.

3D: Rendering to expected locations

In 3D, it’s important to render to the following location

<project_directory>/renders/units/<unit>/<animation><version>/

First note the /units subfolder under the /renders folder. This is mandatory token.

Then <unit> represents the name of the sprite unit (‘hunter’).

<animation> represents that particular animation name (‘walk’)

Though there is a <version> token, this doesn’t have to be used, though it is much better to use it because you can juggle around variations quickly.

When placed properly, you should get something like this:

2017-02-23-08_43_27-c2-sprite-manager-b1-_-r__game_projects_citizen_demo_b_c2_c2-caproj

In the Sprite list, note ‘Fr’ is 0. This means there are no frames there. In order for any frame to register as proper frames, they need to be named thus:

NULL.0001.png

Note ‘IP’ is also 0. This means that there either have been no image-points generated. The number of IP should be the same as the number of Fr.

Note ‘Crop’ is 0, which means no cropped images have been detected.. ‘CropIP’ is the same deal.

Lastly, note that the C2-related parameters like ‘Spd’, ‘Loop’, etc, are -1. -1 means that no change should be done when you hit ‘Commit caproj’. If this value is larger than -1, then that value will be set in the .caproj as your desired value.

Let’s say now that some frames were rendered from the 3D application, and move on.

C2SpriteMan: Previewing, settings attributes

This step can be done at any point even after cropping.

You can preview the animation using djv. It takes the ‘Spd’ parameter and uses that as the framerate. If ‘Spd’ is -1, then djv will play it back with whatever default it selects.

On the far-right side, an Attributes panel shows the fields which you can adjust parameters of selected sprites. Hitting the blue button ‘Apply’ will apply your changes onto the sprites. These changes are stored in their respective unit folder, so that these settings are portable and will carry over as long as you keep those files in the folders they are meant to be in.

3D: Generate imagepoint files

Generating imagepoint files in LW means getting the projected camera-view space coordinates of a particular 3D location. I’ve done this in LW using LScript but this is a 3D-specific solution. I’ve not it in Maya, and I reckon it would be a different solution.

Either way, the generation of imagepoint files must follow the rules outlined in the first section. Once the imagepoint files are done, C2SpriteMan should reflect a corresponding value in the ‘IP’ column.

C2SpriteMan: Croppping

After generating imagepoint files, the next step is to crop the sprites. This not only crops the sprites but it also ‘crops’ the imagepoint files accordingly, taking the values there and trimming them so that the cropped imagepoint files will have values that register with their corresponding cropped spirtes.

C2SpriteMan: Copying

The next step is to immediately copy the sprites over to the C2 folder.

C2SpriteMan: Commit

After copying, you can then commit.

First, select the unit animations you want to effect. Then hit ‘Commit caproj’. This function looks at all the settings of the selected animations and goes through the .caproj, parses it, and applies the XML settings.

Hotspots and image-point attributes are derived from the .imagepoint files, while the frame-related settings are derived from the other individual settings inside the 3D unit folder.

Frame duration is applied based on the number of CROP sprites detected in the 3D unit folder.

C2: Check sprites

Open up C2, and confirm the settings.

Advertisements

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.

… and the money is in the taking

(Part 2)

I see a kind of game devs that have a particular preoccupation with monetization. When I say monetization, I generally mean the kind that doesn’t just about selling your stuff on Steam.

Monetization brings a game dev into another framework. When a game dev has an issue with a component in their game that affects their income, it is not an issue they can simply forget. They have chosen a path and decided what their bottom line is. There is, on one side, the hobby of tinkering with games’ nuts and bolts, and on the other, the figuring out how you can be monetarily be rewarded from your efforts. Each balances according to their own tastes.

This points back to the first part of what I was saying. It seems to — partly anyway — explain why there is a such a big fuss when software doesn’t turn out the way you want. For professionals, there’s no time to screw around with incomprehensible decisions, and frustrations grow out of a need to produce results, and that is being undermined by some third party. It’s your job, your paycheck, your reputation at stake. It’s serious business, as all real businesses are. When you make your hobby into a business, it becomes a business, and it won’t feel like a hobby any more.

Game development — with its big studio and indie counterparts — is a whole different world. They have their own denizens, atmosphere, and grading system. It’s far from a desirable world, and heck, it’s not even particularly better than my boring one.

If there is a solace, a quiet place, a haven, where as a kid you dreamed about fascinating worlds, it’s not here, nor there, but temporally offset.

The fun is in the making…

There are two kinds of people. People who like to play, and people who like the make things.  And among those who like to make things, I can see two more kinds of people between them:

  1. Those who like making stuff.
  2. Those who like having made it.

Those who like making stuff, consequently, eventually — ideally — like the fact they made it.

And then there is a kind of person who prefers to have done it already. Of course, if it were already done before he started on it, then he couldn’t be considered a creator — and he wants to create. But if he could do it with a push of a button, or use an Imagino-matic device, that would be the most ideal situation.

But how much work does he want to do? Does he want to work just enough to feel like he’s earned a six-pack from a 2-minute calisthenic workout? Or is it like giving birth to a baby?

There’s more than a line, or a degree, or a quantification that crosses the boundaries of convenience and perseverance. How much automation is there before it’s actually automation? How much are we really putting in for the amount we’re getting back?

The nature of software is that we build on top of one another. We don’t write Assembly because there is no practical benefit to it.  But now, I ask a different question: When it comes down to it, would you be willing to start from scratch? It’s not about delineating degrees of laziness versus masochism. Instead, it is an attitude, an approach to life and learning.

There is absolutely nothing wrong with taking an easier route, especially if you took ‘hard’ to get to ‘easy’: you’re worth your own weight, like any good SAS trooper. But some people don’t want to take ‘hard’ at all. They just want to be shown a way that produces results. They don’t see problems as natural curiosities; for them, they’re irritants, not accelerants.

In the CG industry where I work, there are many varied roles, and people vary a lot in this regard. An animator has animation skills and he doesn’t have the inclination to rig a character, and I wouldn’t hold it upon him to do so. But I would expect him to be committed to all things animation. A modeller may not take interest in matchmoving, but his anatomy should be solid.

So it surprises me that there are so-called game devs who feel insulted to have been forced to troubleshoot their own game-related problems. But aren’t game devs supposed to regard game development problems the very point of game development? Isn’t this what ‘making a game’ is all about? Isn’t this actually the fun part?  ‘Making my game’ is supposedly what we enjoy. But in fact, what some people actually mean is ‘Seeing my game made’.

I don’t know. Maybe I think too old-school. I look at 2400AD and think Chuck Bueche, and the whole lot of them back then, were having loads of fun playing around with bits and pixels. And I am having tons of fun, too, and every bit and byte grateful that in this day and age we have such an easy time making games. But the thing is, I wouldn’t mind it at all if it were much harder.

Adding Image Object support to TMX Importer V2

Although I’ve requested this from Rex, and still waiting on a possible reply, I’ve gone ahead and tried to hack the plugin in order to learn something from it.

The main thing I want to do is get the Tile ID used by the Image Object. Tiled has the capability of using Tile images in Objects. I’ve recently realised that Tiles may not work for me in aspects where I am placing Tiles/images larger than the given map’s tile size. In isometric tiles, the the lower-left hand side is aligned with the lower-left limits of the ‘diamond’ tile. The taller Tile I use, the higher the offset will be when I try to place the Tile onto the grid. The wider the tile, the farther to the right the bottom tip of the ‘diamond’ gets from the grid tile’s anchor point.

In other words, alignment is the reason why I don’t want to use Tiles when there are tile sprites larger than the grid size.


I realised that I was trying to do this before because I saw a comment I put in the code near where I added something to get the ‘gid’ of an object. Apparently, I had forgotten that I had come to the same need before.


TMX Importer V2

First step was to find out how to get the ‘gid’, which is an xml attribute. I looked for other properties, like how Objects’ names are retrieved and follow the same procedure.

In runtime.js, I find:

instanceProto._read_obj = function (obj)
{
 if (!obj)
 return false;
 
 this.exp_ObjectName = obj.name;
 this.exp_ObjectType = obj.type;
 this.exp_ObjectPWidth = obj.width;
...
}

this.exp_ObjectName is populated by the obj.name. So I immediately just put in:

this.exp_ObjectGID = obj.gid;

Then, of course, I’ve got to be able to add the member .gid somewhere. So I look for obj by looking at who called _read_obj().

I see _retrieve_objects():

 instanceProto._retrieve_objects = function()
 {
 var obj_groups = this._tmx_obj.objectgroups;
 var i, group, group_cnt=obj_groups.length;
 var j, obj, objs, obj_cnt;
 var x,y;
 for (i=0; i<group_cnt; i++)
 {
 group = obj_groups[i];
 this.exp_ObjGroupName = group.name;
 this.exp_ObjGroupWidth = group.width;
 this.exp_ObjGroupHeight = group.height; 
 objs = group.objects;
 obj_cnt = objs.length;
 for (j=0; j<obj_cnt; j++)
 {
 this._read_obj(objs[j]);
 this.runtime.trigger(cr.plugins_.Rex_tmx_importer_v2.prototype.cnds.OnEachObject, this); 
 }
 }
 };

I’ve emphasised the key bits. Ultimately, objs[] is populated by groups which taken from the _tmx_obj. So I look for that.

instanceProto.ImportTMX = function(tmx_obj)
{ 
 this._tmx_obj = tmx_obj;
...
}

This imports the tmx and populates the _tmx_obj. It takes a tmx_obj as an arg, so I look at all calls to ImportTMX().

There are two instances. This is one of them:

instanceProto.import_tmxObj = function (source, parser)
 {
 var tmx_obj = parser.TMXObjGet(source); 
 this.ImportTMX(tmx_obj);
 
 this.tmx_source = source;
 this.parser_uid = parser.uid;
 };

I know that this is something to do with the TMX Parsers (XML/JSON), so I go over to those plugins and open up their runtime.js. The idea here, so far, is that the Parsers get XML info. So what I need, specifically, is to read that ‘gid’ attribute from the XML.

Go to XML Parser

I look for the code that gets properties from Objects (vs Tiles). I find it here:

var _get_object = function(xml_obj, xml_object)
 { 
 var object = {};
 object.name = xml_obj.get_string_value("@name", xml_object);
 object.type = xml_obj.get_string_value("@type", xml_object); 
 object.x = xml_obj.get_number_value("@x", xml_object);
 object.y = xml_obj.get_number_value("@y", xml_object); 
 object.width = xml_obj.get_number_value("@width", xml_object);
 object.height = xml_obj.get_number_value("@height", xml_object);
 var xml_properties = xml_obj.get_nodes("./properties/property", xml_object);
 object.properties = _get_properties(xml_obj, xml_properties);
 return object;
 };

From here I reckon that the syntax of the ampersand (@) gets the attribute name. So I add ‘gid’.

...
// lernie
 object.gid = xml_obj.get_number_value("@gid", xml_object);
...

Back to TMX Importer V2

Then I look over other member names to make sure I’ve init the variables like everyone else.

instanceProto.onCreate = function()
 {
...
this.exp_ObjectGID = 0;
...
}

I also see another reference (using ‘ObjectName’ as my search term)

Exps.prototype.ObjectName = function (ret)
 { 
 ret.set_string(this.exp_ObjectName);
 };

So I add that for ‘ObjectGID’

Exps.prototype.ObjectGID = function (ret)
 { 
 ret.set_int(this.exp_ObjectGID);
 };

I’m deducing that this connects the expression with the actual value of the member during runtime.

I know that there is still an edittime.js I need to attend to, so I go over there.

Go to edittime.js

I look for the same references (eg ‘ObjectName’) so I can add new stuff in.

I see:

// Expressions
...
AddExpression(55, ef_return_number, 
 "Get logical Y index of object", "Object: Object", "ObjectY", "Get logical Y index of object.");
...

So I add one in

AddExpression(59, ef_return_number, "Get GID of Object", "Object: Object", "ObjectGID", "Get GID of Object.");

I increment the first int, use ef_return_number which is likely a constant for the return value. The 4th arg is a unclear at the moment. The 5th arg “ObjectGID” is the actual expression keyword. This seems to tie in with runtime.js’s ‘expression prototypes’:

Exps.prototype.ObjectGID = function (ret)
 { 
 ret.set_int(this.exp_ObjectGID);
 };

Where ‘ObjectGID’ is like a run-time replacement (like LScript pragmas), in which the name of the expression keyword is tied with the internal member name of exp_ObjectGID.

When I test this, it works. So, now I’ve successfully retrieved the ‘gid’ of the Object being tested for.

Now the next step is to find out the TilesetName property (among other things) of the Image Object. I know that the ‘gid’ refers to the TileID. So I look at how Tile IDs are connected with a Tileset, and from there, get the Tileset’s name.

I search for the ‘TilesetName’, and the one of interest comes up:

instanceProto._read_tile_at_LXY = function(tmx_layer, x, y, is_raw_data)
{
...
var tileset_obj = this._tmx_obj.GetTileSet(this.exp_TileID);
 this.exp_TilesetName = tileset_obj.name;
 this.exp_ImageSource = tileset_obj.image.source;
 this.exp_ImageWidth = tileset_obj.image.width;
 this.exp_ImageHeight = tileset_obj.image.height;
 this.exp_TilesetProperties = tileset_obj.properties;
 var tile_obj = tileset_obj.tiles[this.exp_TileID];
 this.exp_Frame = this.exp_TileID - tileset_obj.firstgid;
 this.exp_TileProperties = (tile_obj != null)? tile_obj.properties: null;
...
}

I see the to assign exp_TilesetName you need the Tileset Object, and that’s accessed through

var tileset_obj = this._tmx_obj.GetTileSet(this.exp_TileID);

So I thought that if I simply use the ObjectGID, instead of the Tile ID as passed above, then I will be good to go. I just needed to find where to place it.

I go back to where I had assigned ObjectGID:

instanceProto._read_obj = function (obj)
{
...
this.exp_ObjectGID = obj.gid;
var tileset_obj = this._tmx_obj.GetTileSet(obj.gid);
if (tileset_obj != null)
{
this.exp_ObjectTilesetName = tileset_obj.name;
}
}

Code works. However, I originally got an error accessing the ‘name’ variable, so I had to test the validity of the tileset_obj, because it may return null, causing the error.

When this worked, I copied all the other members from the Tilesets properties and adapted them for Image Object:

if (tileset_obj != null)
 {
 // alert (tileset_obj.name);
 this.exp_ObjectTilesetName = tileset_obj.name;
 
 this.exp_ObjectImageSource = tileset_obj.image.source;
 this.exp_ObjectImageWidth = tileset_obj.image.width;
 this.exp_ObjectImageHeight = tileset_obj.image.height;
 this.exp_ObjectTilesetProperties = tileset_obj.properties;
 var tile_obj = tileset_obj.tiles[this.exp_ObjectGID];
 this.exp_ObjectFrame = this.exp_ObjectGID - tileset_obj.firstgid;
 this.exp_ObjectTileProperties = (tile_obj != null)? tile_obj.properties: null;
 }

Conclusion/Summary/Points/What I’ve Learned

  1. The TMX parser is the one that populates the TMX Object so any new data needed must be checked there to see whether it can be properly accessed. After the TMX Object is populated the runtime object (this) can get that info for its own members (eg self.exp_ObjectGID).
  2. Expressions are registered in the edittime.js. This is required because C2 won’t allow you to write any expression variable just like that. The expression is referenced back into the runtime from the Exps.prototype.ObjectGID = function(ret) {..} mentioned above. This function seems to return the self member, so it seems like the retriever of values from the runtime object into the game. I could be wrong, though. 🙂
  3. instanceProto is basically ‘this’, and it used to declare functions, which are then called through self.myFunc(). If you trace any function, it will end up as being declared by this.
  4. In the case of the Objects, they were being retrieved, and if traced back, it all started from the Action:Create tiles.

 

 

 

 

Calculate camera angle for tile width/height

import math
def GetIsoCamPitch(w=256,h=128):
 r = float(float(h)/float(w))
 return math.degrees(math.asin(r))
 
def GetTileSizeFromCameraPitch(a=45,w=0,h=0,ret='r'):
 '''
 Get the ratio, w, or h, from camera's pitch a
 If w is provided then h is ignored, vice-versa
 '''
 r = math.sin(math.radians(a))
 if 'r' in ret:
 return r
 if 'w' in ret:
 # this assumes that h was provided for
 return 1/r*h
 if 'h' in ret:
 #assumes that w was provided
 return w*r

print GetTileRatioFromCameraPitch(35,w=256,ret='h')

Code (Python) to calculate the camera down (pitch) angle to achieve a Tile width/height ratio, and get tile ratio from camera pitch.