"Script-Defined" Image Maps (Tech Demo 3)
by Charlie Patterson · 05/03/2012 (12:23 pm) · 19 comments
Hey everyone,
I thought I'd document a few of my tech successes updating the Torque2D engine. This one is about creating ImageMaps (sprite sheets) with variable-sized frames. (Skip to the end for a video.)
This code has been published here: www.garagegames.com/community/forums/viewthread/130503
(Also, if you enjoy this tech demo, please consider "paying forward" with a click on my game's like button, here: facebook.com/RednecksVsRobots. Gonna look for some funding soon and likes will help, I hope!)
Specifically, I use it to read an XML file, which has the same name as the image map, and which acts as a "manifest" of all the frames in the image. So for instance, robotPuppet.png will have a matching robotPuppet.xml. I can then use TorqueScript's XML functions to parse the file and call addFrame() for each frame listed in the manifest.
The idea came about when using Texture Packer. This is a great tool (and cheap) for creating images maps in many different formats. It excels at, well, packing the textures tightly, even allowing for rotation if that helps fit more textures on the sheet. One of its output formats is a "generic XML" format which is what I use now.
I'll show an example in a second, but first I want to be fair about the downsides... asking Torque to call your script can be confusing if your script has bugs or doesn't handle some problem with a corrupted file. The game, or the editor, isn't "up" yet when it preloads images, so the only output is in the console.log file. This is fine for us server programmers, but a little odd when using visual tools. Torque Game Builder, or you game, may start with the images botched. And if you screw something up, it won't be very obvious. In my defense, Torque doesn't have too many easy ways to inform you that it is having troubles and allow you to repair it in real-time.
In any case, I wanted to do something generic, so I stuck with the idea of calling your script and receiving the frames in return. You could, for instance, create many kinds of XML files, or even files in some other format. You could not even have a "manifest" file at all, and instead come up with your own variation of key mode (although you wouldn't have any tools to read the image pixels to find colors and what-not). Or perhaps you want one big manifest for all your images. Whatever works.
So once again, without further philosophizing, here is some demo material. Since I haven't found a way to show images on the GarageGames blog server, I'm going to just show the XML output that goes with a robot file. The images are in the video later. For this robot, the "frames" are the individual components (treads, body, ...) that make up the robot and the robot has two different sets of pieces for two different animation modes, so you'll see "attack" parts and "walk" parts:
As a bonus, I'd like to keep these names for the parts around. Because Torque allows you to add fields to *any* object, including an ImageMap, I create an array in the ImageMap that was passed to my load function. So I'll have frameNames[0]=attack_antenna_1, etc. I can then cross reference this for the original name.
And here is some snippets (for expediency) from my "Generic XML Loader":
That's it. Here's a video. :)
I thought I'd document a few of my tech successes updating the Torque2D engine. This one is about creating ImageMaps (sprite sheets) with variable-sized frames. (Skip to the end for a video.)
This code has been published here: www.garagegames.com/community/forums/viewthread/130503
(Also, if you enjoy this tech demo, please consider "paying forward" with a click on my game's like button, here: facebook.com/RednecksVsRobots. Gonna look for some funding soon and likes will help, I hope!)
torque2D ImageMap types
As you probably know, Torque has two ways to retrieve the frames, or individual images, out of an ImageMap. Those two ways are "cell" and "key" modes:- ) Cell Mode - You must make sure all your frames are the same size, say 100x50. Then you use the "cell" mode on an ImageMap and tell Torque2D that your images are each 100x50. It will break the ImageMap down accordingly. If you have varied sized images to store, you'll have to either pad them with empty space or try something else. But I assume many basic sprite animations can use same-sized sprites without much waste. These are also usually easier to "register" or match one to the next on-screen. You simply keep the rectangle positioned so that the animation of the frames plays out "in the middle" like a flipbook.
- ) Key Mode - You can use a unique "key" color and use the fairly sophisticated "key" mode on an ImageMap. In short, the first pixel of the image defines the key color, say pink. Then frames are stored in rows in the map. If the frames aren't the same height and/or width, that's OK. As long as you follow certain rules, frames will be found. This is better on space, and it does allow you to have different-sized frames; but I haven't found any pro tools that can save in this format! Within Torque Game Builder -- the editor -- key mode seems to be mostly used with bitmaps for GUIs, which are often small little thumb buttons and close boxes and the like all smashed into one small image. Ultimately they should save space but it could be more efficient.
my new "script-defined" image map frames
The idea behind the "scripted" mode ImageMaps is that for every image map Torque should call your code and you will define where the frames are, dynamically. Once an image map is set to script mode, your game (and the Torque Game Builder) will call you once per image map. You'll get a reference to the ImageMap, and the name of the image file passed to your callback, but that's all the engine really knows. From here, you run your method and call addFrame(index,x,y,w,h) once per frame. When your method returns, Torque assumes all the frames are defined and moves on.Specifically, I use it to read an XML file, which has the same name as the image map, and which acts as a "manifest" of all the frames in the image. So for instance, robotPuppet.png will have a matching robotPuppet.xml. I can then use TorqueScript's XML functions to parse the file and call addFrame() for each frame listed in the manifest.
The idea came about when using Texture Packer. This is a great tool (and cheap) for creating images maps in many different formats. It excels at, well, packing the textures tightly, even allowing for rotation if that helps fit more textures on the sheet. One of its output formats is a "generic XML" format which is what I use now.
I'll show an example in a second, but first I want to be fair about the downsides... asking Torque to call your script can be confusing if your script has bugs or doesn't handle some problem with a corrupted file. The game, or the editor, isn't "up" yet when it preloads images, so the only output is in the console.log file. This is fine for us server programmers, but a little odd when using visual tools. Torque Game Builder, or you game, may start with the images botched. And if you screw something up, it won't be very obvious. In my defense, Torque doesn't have too many easy ways to inform you that it is having troubles and allow you to repair it in real-time.
In any case, I wanted to do something generic, so I stuck with the idea of calling your script and receiving the frames in return. You could, for instance, create many kinds of XML files, or even files in some other format. You could not even have a "manifest" file at all, and instead come up with your own variation of key mode (although you wouldn't have any tools to read the image pixels to find colors and what-not). Or perhaps you want one big manifest for all your images. Whatever works.
demo
So once again, without further philosophizing, here is some demo material. Since I haven't found a way to show images on the GarageGames blog server, I'm going to just show the XML output that goes with a robot file. The images are in the video later. For this robot, the "frames" are the individual components (treads, body, ...) that make up the robot and the robot has two different sets of pieces for two different animation modes, so you'll see "attack" parts and "walk" parts:
<?xml version="1.0" encoding="UTF-8"?>
...
<TextureAtlas imagePath="generic.png" width="1011" height="1080">
<sprite n="attack_antenna_1" x="733" y="826" w="27" h="98"/>
<sprite n="attack_clawBottom_1" x="325" y="939" w="78" h="82"/>
<sprite n="attack_clawTop_1" x="662" y="826" w="69" h="100"/>
<sprite n="attack_drill_1" x="650" y="939" w="117" h="75"/>
... you get the idea
<sprite n="walk_clawBottom_1" x="2" y="939" w="77" h="85"/>
<sprite n="walk_clawTop_1" x="512" y="826" w="71" h="107"/>
<sprite n="walk_foreArmBack_1" x="585" y="826" w="75" h="102"/>
<sprite n="walk_forearmFront_1" x="179" y="826" w="160" h="108"/>
... and so forth
</TextureAtlas>As a bonus, I'd like to keep these names for the parts around. Because Torque allows you to add fields to *any* object, including an ImageMap, I create an array in the ImageMap that was passed to my load function. So I'll have frameNames[0]=attack_antenna_1, etc. I can then cross reference this for the original name.
And here is some snippets (for expediency) from my "Generic XML Loader":
// the generic function that always gets called for all script-defined image maps
function onDefineImageMapFrames(%imageMap, %bitmapPath)
{
// record that we got here and why
echo("onDefineImageMapFrames: ", %imageMap, " from ", %bitmapPath);
// build up the file name, such as images/robotPuppet.xml
%imageMapAtlasName = filePath(%bitmapPath) @ "/" @ fileBase(%bitmapPath) @ ".xml";
%xml = new SimXMLDocument();
%xml.loadFile( %imageMapAtlasName ); // skipping error checking
%xml.pushFirstChildElement( "TextureAtlas" )
// basically calls "loadTexturePackerXMLSprite" once every time an XML <sprite> is found
YepUtil::callPerXMLChild(%xml, "sprite", 0, loadTexturePackerXMLSprite, %imageMap);
%xml.delete();
}
function loadTexturePackerXMLSprite(%xml, %imageMap)
{
// check out the example Texture Packer XML output to see what I'm grabbing here
%name = %xml.attribute("n");
%x = %xml.attribute("x");
%y = %xml.attribute("y");
%width = %xml.attribute("w");
%height = %xml.attribute("h");
// error checking removed
// we want the frame "names" so store them in the ImageMap.
%imageMap.frameNames[$onDefineImageMapFrames::TPFrameNumber] = %name;
%imageMap.addFrame($onDefineImageMapFrames::TPFrameNumber, %x, %y, %width, %height);
$onDefineImageMapFrames::TPFrameNumber++;
}That's it. Here's a video. :)
#2
05/03/2012 (2:14 pm)
Thanks @steve. Is it better now? I did post the blog before fixing that, but it's been fixed a while now.
#3
05/03/2012 (6:09 pm)
Excellent job Charlie!
#4
05/04/2012 (5:44 am)
Thanks Charlie, I was thinking about something similar, you've saved me a lot of effort. What is the YepUtil::callPerXMLChild ?
#5
05/04/2012 (5:49 am)
Will this work with iTorque2D?
#6
05/04/2012 (11:11 am)
@Charlie, are there any plans to make this into a resource, I would really like to try and get this working in iTorque2D?
#7
P.S. Since I hadn't planned on it, I'd have to do a few steps to get it ready.
05/04/2012 (12:07 pm)
Hi @Scott, aka the cannibal dude. I hadn't *planned* on it, but I could work on that early next week. I don't see why it wouldn't work in iTorque as well. If you choose to accept this mission, it would be a patch that affects both the engine and the TGB editor script.P.S. Since I hadn't planned on it, I'd have to do a few steps to get it ready.
#8
05/04/2012 (12:24 pm)
Thanks Charlie, mission accepted :) I think it would be a very useful resource as many of us are hitting memory limitations on the iDevices.
#9
Still, the design of the key-mode format can't be quite as efficient as truly "bin packing" everything as tightly as possible.
05/04/2012 (1:14 pm)
Oh, also, I was writing back and forth with the creator of TexturePacker, Andreas Low, and he will probably add Torque's "key-mode" animations specifically for Torque, so that'll be cool! Might take a little while, though, as he's busy.Still, the design of the key-mode format can't be quite as efficient as truly "bin packing" everything as tightly as possible.
#10
I looked at key mode as well and kind of hoped TexturePacker would have something.
05/04/2012 (1:35 pm)
Andreas is a good guy. He gave me an early version of the custom exporter and I wrote an exporter that was perfect for our projects - it truly saved me hours of work.I looked at key mode as well and kind of hoped TexturePacker would have something.
#11
05/05/2012 (4:10 am)
@Charlie, thinking some more ... one thing I need to do will be to have animations that are made from cells of different sizes. I'm thinking if we have the scripted approach we can extend the animation player and select and size the cells as the animation plays? Does this sound feasible?
#12
Torque thinks of everything from its center which will make it a little more devilish than if everything where from the top, left corner.
So somehow you will have to save "the center" or some other registration point per image, even though the clipped image will probably have a different physical center.
The good news is that Texture Packer will do the clipping for you and save the offsets (amount clipped) in the manifest file. What I've been doing in some cases is receiving the animations in a regular-sized box say 400x400. If you were to play the images in a square scene object it would look just right. Then I let Texture Packer clip them down for me, and save the offsets in the manifest. Then when I load the animations, I can save read offsets into Torque. Now I know how much to size and position the scene object to make it look like it did before there was clipping.
I've been able to do this in script so I don't have to extend the t2dAnimatedSprite class and yet it is fast enough for me. I just make a behavior to do such things generically onUpdate()
05/05/2012 (3:12 pm)
@Scott, yes that sounds feasible... however I've been playing with this, too, and the confusing thing will be "registering" the images -- or translating irregular images so that they line up correctly.Torque thinks of everything from its center which will make it a little more devilish than if everything where from the top, left corner.
So somehow you will have to save "the center" or some other registration point per image, even though the clipped image will probably have a different physical center.
The good news is that Texture Packer will do the clipping for you and save the offsets (amount clipped) in the manifest file. What I've been doing in some cases is receiving the animations in a regular-sized box say 400x400. If you were to play the images in a square scene object it would look just right. Then I let Texture Packer clip them down for me, and save the offsets in the manifest. Then when I load the animations, I can save read offsets into Torque. Now I know how much to size and position the scene object to make it look like it did before there was clipping.
I've been able to do this in script so I don't have to extend the t2dAnimatedSprite class and yet it is fast enough for me. I just make a behavior to do such things generically onUpdate()
#13
For iT2D I think I will have to extend the animated sprite class as the iPhone doesn't have the necessary performance to render in script :(
This is what I was thinking (I think your mechanism is very similar):
- dump all the individual animation cells into TP
- export using generic exporter
- we use the sprite name to define the animation strip e.g. walking_1, walking_2, walking_3 etc
- the t2dAnimationDatablock has a new attribute which is the stripName e.g. stripName = "walking"
- we also store within the data block the x,y,w and h attributes for each cell
- when the animation plays we use the strip name and frame index to reference the cell within the sheet. The width and height attributes are used on a per cell basis to ensure that the perspective is correct.
e.g.
- get walking 1 from sheet
- get w/h for walking 1
- set size for walking 1
- render walking 1
- get walking 2 from sheet
- get w/h for walking 2
- set size for walking 2
- render walking 2
etc
Thoughts?
05/05/2012 (6:05 pm)
I think the centre point works to our advantage? I just did a test with an animation sheet - split it up into individual clipped images, loaded them into the editor. I then set the position to be the same for all of them and as far as I can tell the animation would be correct, despite the slightly different sizes.For iT2D I think I will have to extend the animated sprite class as the iPhone doesn't have the necessary performance to render in script :(
This is what I was thinking (I think your mechanism is very similar):
- dump all the individual animation cells into TP
- export using generic exporter
- we use the sprite name to define the animation strip e.g. walking_1, walking_2, walking_3 etc
- the t2dAnimationDatablock has a new attribute which is the stripName e.g. stripName = "walking"
- we also store within the data block the x,y,w and h attributes for each cell
- when the animation plays we use the strip name and frame index to reference the cell within the sheet. The width and height attributes are used on a per cell basis to ensure that the perspective is correct.
e.g.
- get walking 1 from sheet
- get w/h for walking 1
- set size for walking 1
- render walking 1
- get walking 2 from sheet
- get w/h for walking 2
- set size for walking 2
- render walking 2
etc
Thoughts?
#14
I'm in a hurry today, but I tried to drop a series of patches that implement this feature. They are on the forum which is the only choice on GarageGames which seems to allow a large file!
www.garagegames.com/community/forums/viewthread/130503
Lemme know if I need to update something! Sorry I'm being so quick.
P.S. Usually, Tortoise Subversion has created patches where the file names have relative paths in them. This time it didn't. It just dumped the filename bare. Hmmm. (I'm using an upgrade.) If this isn't good, I'll have to figure something out.
05/08/2012 (1:59 pm)
@Scott et al.I'm in a hurry today, but I tried to drop a series of patches that implement this feature. They are on the forum which is the only choice on GarageGames which seems to allow a large file!
www.garagegames.com/community/forums/viewthread/130503
Lemme know if I need to update something! Sorry I'm being so quick.
P.S. Usually, Tortoise Subversion has created patches where the file names have relative paths in them. This time it didn't. It just dumped the filename bare. Hmmm. (I'm using an upgrade.) If this isn't good, I'll have to figure something out.
#15
05/08/2012 (2:12 pm)
@Charlie, no probs, I'm working this week so won't get to them until the weekend. I'll take a look at the files to see if I can understand what is going on. Cheers!
#16
05/11/2012 (5:36 pm)
@Charlie, this works a treat thank you. I have adapted it to allow the playing of animations from the sprite sheet and handle the situation where the animation frames are different sizes.
#17
Part 1
t2dAnimatedSprite.h
Add the following boolean
Part 2
t2dAnimatedSprite.cc
Change the default constructor:
Part 3
t2dAnimatedSprite.cc
Add a console method to switch on auto sizing.
05/11/2012 (5:40 pm)
These are the code changes to support the playing of different sized animation frames from a TexturePacker sprite sheet (only tested with iTorque2D).Part 1
t2dAnimatedSprite.h
Add the following boolean
bool mAutoSize;
Part 2
t2dAnimatedSprite.cc
Change the default constructor:
t2dAnimatedSprite::t2dAnimatedSprite() : T2D_Stream_HeaderID(makeFourCCTag('2','D','A','S')),
mAnimationCallbackComplete(true),
mFrameChangeCallback(false),
mAutoSize(false)Part 3
t2dAnimatedSprite.cc
Add a console method to switch on auto sizing.
//-----------------------------------------------------------------------------
// Auto set the size of the current frame based on the original frame size
// in the image map.
//-----------------------------------------------------------------------------
ConsoleMethod(t2dAnimatedSprite, setAutoSize, void, 3, 3, "(bool enabled) - Auto set the size of the current frame based on the original frame size.n"
"@param enabled Whether or not to switch on auto sizing of the animation frame.n"
"@return No return value.")
{
object->mAutoSize = dAtob(argv[2]);
}
#18
t2dAnimatedSprite.cc
Find the method integrateObject
Add the auto sizing code after the frame change callback code.
05/11/2012 (5:48 pm)
Part 4t2dAnimatedSprite.cc
Find the method integrateObject
Add the auto sizing code after the frame change callback code.
// Are we using frame-change callback and the frame has changed?
if ( mFrameChangeCallback && frameChanged )
{
// Yes ...
// Argument Buffer.
static char argBuffer[16];
// Format Event-Modifier Buffer.
dSprintf(argBuffer, 16, "%d", mAnimationController.getCurrentFrame() );
// Call Scripts.
if( isMethod( "onFrameChange" ) )
Con::executef(this, 2, "onFrameChange", argBuffer);
}
// Are we auto-sizing?
if (mAutoSize && frameChanged ) {
// Set the size to the frame.
const t2dImageMapDatablock::cFramePixelArea& pixelArea =
mAnimationController.getCurrentFramePixelArea();
t2dVector size = t2dVector(pixelArea.mWidth, pixelArea.mHeight);
setSize(size);
}
#19
Usage:
1. Use Charlie's resource for scripted image maps.
2. Add the animation changes.
3. Define your image map, animation block etc:
4. Create a t2dAnimatedSprite as per usual.
5. Switch on the auto sizing for the animation.
That's it!
05/11/2012 (5:52 pm)
Part 5Usage:
1. Use Charlie's resource for scripted image maps.
2. Add the animation changes.
3. Define your image map, animation block etc:
new t2dImageMapDatablock(elviraEmotionImageMap) {
imageName = "~/data/images/scripted/elvira-emotion" @ $retinaIpad;
imageMode = "SCRIPTED";
frameCount = "-1";
filterMode = "SMOOTH";
filterPad = "0";
preferPerf = "1";
cellRowOrder = "1";
cellOffsetX = "0";
cellOffsetY = "0";
cellStrideX = "0";
cellStrideY = "0";
cellCountX = "-1";
cellCountY = "-1";
cellWidth = "2048";
cellHeight = "2048";
preload = "1";
allowUnload = "1";
compressPVR = "0";
optimised = "1";
force16bit = $FORCE_16BIT_MENU_LEVEL;
};
new t2dAnimationDatablock(elviraEmotionAnimationDataBlock) {
imageMap = "elviraEmotionImageMap";
animationFrames = "0 1 2 3 4 5 6 7 8";
animationTime = "1";
animationCycle = "1";
randomStart = "0";
startframe = "0";
pingPong = "0";
playForward = "1";
};
new t2dSceneObjectDatablock(elviraEmotionAnimation) {
animationName = "elviraEmotionAnimationDataBlock";
};4. Create a t2dAnimatedSprite as per usual.
%this.elvira = new t2dAnimatedSprite() {
config = "elviraEmotionAnimation";
scenegraph = %this;
position = "0 0";
layer = 0;
};5. Switch on the auto sizing for the animation.
%this.elvira.setAutoSize(true);
That's it!

Torque 3D Owner Steve Way
Default Studio Name