Game Development Community

dev|Pro Game Development Curriculum

Sprites in Torque TGE1.5.2 Part 1

by Wesley Hopson · 10/12/2010 (12:05 pm) · 11 comments

ChangeLog
10-15-10: Added a video of the completed version of the code and reorginized the resource
10-16-10: Found a bit of code i forgot to add in oops...
11-12-10: Bug Fixed played animation threads was a bit of a problem with the thread lingering at the end or start even if the thread is stopped. They should now appear to start and stop properly though the normal oddities of animations are still there. (Prob shoulda noticed this nearly obvious problem along time ago) Added in support for animation triggers. In theory should allow for footpuffs and the like. (Not got around to testing it yet since i am using the triggers for my own nefarious purposes)
11-17-10: Another Bug Fix related to the Sprite grabbing the Server ID for the camera instead of Client. Because the Server's connection to the client is used instead of the Client's connection to the server Amazing the blatant problems i miss.... Change is in Part2

Introduction:
Forgive me if this is messy and hard to follow it is my first attempt at posting a resource i will try and smooth things out and finish adding all the related code when i get a chance. I guess i am doing this a bit like a tutorial. Sorry if it is a bit long in length too was not sure how to post it in code files plus i wanted to explain what i was doing a bit as i went along. That said it might or might not be
easy for beginner programmers to follow. Also admittedly I take advantage of much of the IFL code to get this to work though AFAIK I have not Broken the IFL's functionality let me know if you have any problems with it other than the Changes to the IFL file type below.


That video demonstrates how the sprite should work after completing part 2 of my resource. The view point is alot closer than i would like but considering the individual frames are only something like 32*55 pixels in dimensions torque does quite a good job to say the least.

Future Plans
I have a bit of a laundry list of things left to do with these sprites still. from some really important things like I really need to look at the memory managment of the sprites since at present the arrays of subdivided textures i do not think are getting deleted properly. Plus certain improvements Including things like support for jump animations, Multiple Layers of Animations and particularly it would also look alot better from say the god view camera from Advanced camera resource. It seems to be a matter of the camera being fixed versus the 3rd person changes the facing when you pan. Unfortunately this is not the only thing i am working on right now either.

What you should know
Before using you should understand the basics of torque and adding resources and compiling them.
Also you should be familar enough with IFL animations to use them. The system all but piggy backs on the IFL code to get started so much of the initial setup is much the same. So i do not explain the procedures for setting up IFL's on a model in Milkshape ect... What i do discuss is what requirements you need as far as sprite sheet/Texture and the DTS and animations on it.

Regarding the dts:
I use a billboard object as the player charecter for my sprites IE a Zbillboard that will always face the camera along the Z axis. The texture on the billboard is the whole image of the sprite sheet to be used. The sheet is broken up into useable sub images by this function. Plus the material of the sprite sheet must be set to translucent. Or Alpha will not work.

**Quick note about the billboard player. I do not use any nodes in my sprites i just could not get joints to work properly with a simple billboard for a player. It was particularly annoying since this would always cause the sprite to be dropped to stomach height through the terrain since it seems the engine will use the middle of the shape for it's bottom point if one is not defined properly. So what I did was i doubled the sprite character vertically and wrote the subdivide code to place the frame on only the top half of the sprite charecter. Bit of a work around however this did have the happy accident of resulting in the sprite pivoting completely from its feet so that the sprite will not appear to change it's position on the ground if the camera angle was to shift all in all it kinda works out.

Regarding the IFL files
This will still load Regular IFL's but you must now deliminate at the start of the file with #IFL to get them to work
Use #AnimList to load the sprites animation information
it uses a file format as such

#delimiter
XCellSize*YCellSize
SeqName Frame1/Duration Frame2/Duration Frame3/Duration

A SeqName ending in an * means that it is cyclic

XCellSize and YCellSize determine how the sprite sheet is divided up it is used in the subdivide code

Also you can optionaly define any number of Animation Triggers for any given sequence in the following manner. Using a - as the deliminator

A proper line would look like this

AnimationName(optional Asterik for Cyclic) -(Animation Frame to be triggered)-(Animation TriggerNum) then the frames and durations

as an example take:
Attack -60-4 -58-3 -3-10 57/25 58/10 59/5 60/25

This Animation when played activate trigger number 3 when frame 58 first is switched too and would also activate trigger 4 when frame 60 shows up. The order of triggers is irrelevant other than it must appear before the first frame on the line. If you notice there is no Frame 3 in this Sequence so trigger 10 will never activate though it can still be safely defined without any problems or complications

There are certain Sequences you want to have defined for your sprite to function properly they should all be cyclic. In order to work all animations need to also be defined in the DTS shape as blank placeholders of the length of frames equal to the number of frames in the file. These special animations though are only relevant once Part 2 is put in. Just for future reference really. These names are case sensitive

Root - The idle thread it's frames are evenly divided by 8 and used as the idle animations to be played while sitting still while facing any direction.**
run - The Forward run animation
back - Opposite direction of forward
Rside - Move Right and to the side
side - Move Left and to the side ***
Lrun - Diagonal between Lside and run
Rrun - Diagonal between Rside and run
Rback - Diagonal between Rside and Back
Lback - Diagonal between Lside and Back

** To be a bit more clear about the root animation it in fact contains 8 different root animation sequences concatenated on to each other in a specfic order. The Code selects from within the master root sequence which one of these 8 segments to play based on the facing of the charecter. The order is such that we have.

Down DownLeft Left UpLeft Up UpRight Right DownRight

In that specfic order. This comes with a few restrictions is that all these directional idle animations while can be any number of frames long. They must each have exactly the same number of Frames so that if you have an idle of 3 frames long you must have a total of 24 frames for 3 for each direction. Also the total lengths of these idle animations must be the same or they will not appear to play properly. You of course can have any durations you feel appropiate for the individual cells so long as there durations each individually add up to be the same. If you feel limited by this keep in mind that while the number of frames has to be the same it makes no bones about identical frames being played back to back so that you can have 27/25 27/25 27/25 as one segment idle animation and 27/20 28/20 29/10 as another.
As far as triggering goes you can define them normally just keep in mind that different parts of the animation get played based on which way you face so some triggers may get activated when you face one way and others another.

Simplest example of the Root with only one frame per direction I used it in the video above
Root* 53/25 49/25 51/25 55/25 52/25 54/25 56/25 50/25

***this one is just side instead of what would be the convention Lside so as to retain compatibility with regular 3d models.


Part 1 code:

starting with the meat and working my way to the loading of the animations from file.

Added to the TextureHandle Class in gTextureManger.h and .cc respectivly

bool mSubdivided; //needs to be defaulted to false in the constructors
void subdivide(U32 divX, U32 divY, const char* Name);



void TextureHandle::subdivide(U32 divX, U32 divY, const char* Name)
{
	if (!mSubdivided)
	{
		mSubdivided = true;
		GBitmap* Source = object->bitmap;
		TextureObject* Frames = new TextureObject[divX * divY];
		char* NameBuffer = new char[50];
		U32 Width = Source->getWidth();
		U32 Height = Source->getHeight();//should be the same as width now.
		U32 FrameSizeX = Width / divX;
		U32 FrameSizeY = Height / divY;
		U32 XResize = getNextPow2(FrameSizeX);
		U32 YResize = getNextPow2(FrameSizeY);
		U32 Xoffset = (XResize - FrameSizeX) / 2;
		U32 Yoffset = (YResize - FrameSizeY);
		YResize = YResize * 2;
		ColorI BasePixel;
		Source->getColor(0,0,BasePixel);
		BasePixel.alpha = 0;
		U32 CellWidth = 0;
		U32 CellHeight = 0;
		U32 Cell = 0;
		ColorI Pixel;
		U32 SourceX = 0, SourceY = 0;
		while (CellHeight < Height)
		{
			CellHeight += FrameSizeY;
			CellWidth = FrameSizeX;
			SourceX = CellWidth - FrameSizeX;
			SourceY = CellHeight - FrameSizeY;
			while (CellWidth <= Width)
			{
				//Begin Copying cell
				GBitmap* Dest = new GBitmap(XResize, YResize, false, GBitmap::RGBA);
				//Con::printf("Bitmap internal format %i", Dest->getFormat());
				U32 X = 0, Y = 0;
				while (Y < YResize)
				{
					X = 0;
					SourceX = CellWidth - FrameSizeX;
					while(X < XResize)
					{
						if ((Y >= Yoffset && Y < Yoffset + FrameSizeY) && (X >= Xoffset && X < Xoffset + FrameSizeX))
						{
							if (Source->getColor(SourceX,SourceY,Pixel) == false)
								Con::warnf("bad Pixel Color in subdivide %i %i",X,Y);
							if (Pixel.red == BasePixel.red && Pixel.blue == BasePixel.blue && Pixel.green == BasePixel.green)
								Pixel.alpha = 0;
							if (Dest->setColor(X,Y,Pixel) == false)
								Con::warnf("could not set Pixel Color in subdivide %i %i",X,Y);
							SourceX++;
						}
						else
						{
							//mark with transparent
							if (Dest->setColor(X,Y,BasePixel) == false)
								Con::warnf("could not set Pixel Color in subdivide %i %i",X,Y);
						}
						X++;
					}
					if (Y >= Yoffset && Y < Yoffset + FrameSizeY)
					{
						SourceY++;
					}
					Y++;
				}
				dSprintf(NameBuffer, 50, "%s_Cell%i", Name, Cell + 1);//so our names matches our animation file
				Frames[Cell] = *TextureManager::registerTexture(NameBuffer, Dest, BitmapSubdividedTexture, false);
				Frames[Cell].Shift = Cell;
				CellWidth += FrameSizeX;
				SourceX = CellWidth - FrameSizeX;
				SourceY = CellHeight - FrameSizeY;
				Cell++;
			}
		}
		//object->bitmap = Dest;
		delete object; //original image no longer required
		delete [] NameBuffer;
		object = Frames;
		//TextureManager::refresh(*this);
	}
	else
		Con::warnf("Should not call subdivide twice");
}

subdivide takes the gBitmap it is given in the texturehandle and divides it into several images in a grid fashion left to right and top to bottom. The XY size of the cells is determined by divX and divY. the Name field is required I cheat in that i send

It should be especially noted that the top left pixel of the Sprite sheet is used as a base transparency color. So any other color in the image of those RGB values will be set to opaque. Was not too keen to work out alpha channels of PNGs for all my sprite sheets.

Then stores them in an array of gBitmaps that is then assigned right back to the textureHandles object field. In this way I pretty much cheat. I do not have to modify in the slightest the actual rendering code of the player. Thus avoiding potential overhead to the rendering code. I just have to increment or decrement the TextureHandle's object pointer accordingly to change the frames that are being displayed.

To ensure we always know where we are shifted too no matter what happens I added this to the TextureObject class also in gTexManager.h

U32 Shift; //the array shift used by sprite animations

Added to tsAnimate.cc

void TSShapeInstance::animateSprites()
{
	for (S32 j=0; j<mThreadList.size(); j++)
	{
		TSThread * th = mThreadList[j];
		if (th->sequence->AnimList)
		{
			TSShape::AnimList* Anim = th->sequence->AnimList;
			TextureHandle* Tex = &this->mMaterialList->mMaterials[Anim->MaterialSlot];
			if (th->pos != 0)
			{
				U8 ShiftMask = 0;
				if (th->Moving == true)
					ShiftMask = (th->sequence->numKeyframes / 8) * th->action - 1;
				 if (th->PrevNameIndex != th->sequence->nameIndex)
				 {
					 //Con::printf("Starting over with a new sequence");
					 th->PrevPos = 0;
					 th->CurFrame = 0;
					 th->CurTrigger = 0;
					 th->PrevNameIndex = th->sequence->nameIndex;
				 }
				if (th->PrevPos > th->pos)
				{
					//Reset the Animation
					th->CurTrigger = 0;
					th->CurFrame = 0;
					Tex->object = Tex->object - Tex->object->Shift + Anim->Frame[th->CurFrame + ShiftMask];
					th->PrevPos = 0;
				}
				if (th->pos < 1)
				{
					th->PrevPos = th->pos;
					if (Anim->KeyFrameTime[th->CurFrame + ShiftMask] <= th->pos)
					{
						th->CurFrame++;
					}
					Tex->object = Tex->object - Tex->object->Shift + Anim->Frame[th->CurFrame + ShiftMask];
					//Advance
					if (Anim->Trigger.size())
					if (Anim->Trigger[th->CurTrigger] == Tex->object->Shift)
					{
						setTriggerState(Anim->TriggerNum[th->CurTrigger] ,true);
						if (th->CurTrigger+1 < Anim->Trigger.size())
							th->CurTrigger++;
						else
							th->CurTrigger = 0;
					}
				}
				else
				{
					//Stop the animation
					//Tex->object -= Tex->object->Shift;
					th->PrevPos = 0;
					th->CurFrame = 0;
					th->CurTrigger = 0;
				}
			}
		}
	}
}

This function keeps track of how we are incrementing and decrementing the pointer with the shiftMask variable. Which of course is quite important to making sure we display the right frames.
You might of noticed i get information from a class inside of TsShape called Animlist i added that to store the animation information for the playing sequence. So inside of TsShape.h and the TsShape class

//AnimList is an alternative to IFL's that allows for more complex 2d animations.
   //it is primarily Useful for things such as charecter sprites and sprite sheets.
   //Features include multiple animations per image face and IFL. Subdivides one texture into multiple keyframes.
   //That are used as an material pool for each possible animation for the shape.
   //Also eventually i hope to be able to overlay two seperate animations on top of each other eventually.
   class AnimList
   {
   public:
	   Vector<S32> Frame;
	   Vector<F32> Duration;
	   Vector<F32> KeyFrameTime;
	   Vector<U32> Trigger;
	   Vector<U32> TriggerNum;
	   U32 MaterialSlot;			//aquired from the IFLmaterial structure before we delete them
	   TSShapeInstance* mShape;

	   AnimList()
	   {
		   VECTOR_SET_ASSOCIATION(Frame);
		   VECTOR_SET_ASSOCIATION(Duration);
		   VECTOR_SET_ASSOCIATION(KeyFrameTime);
		   VECTOR_SET_ASSOCIATION(Trigger);
		   VECTOR_SET_ASSOCIATION(TriggerNum);
	   }

	   ~AnimList()
	   {
	   }
   };

AnimList is the data container where all the relevant info about the frames of a sprites sequence are stored.

I also added some utility variables to TsShapeInstance.h inside of the TsThread class

//These Three are used by AnimList for sprite based animation only
   U32 CurTrigger;				//Current Trigger we are using.  Once triggered once it increments or goes back to zero to allow only one activation
   U32 CurFrame;				//keeps track of which frame of the animation we are on
   U32 CurShift;				//the Shift in blocks of the object pointer we are currently using
   F32 PrevPos;					//about the only way i could think of to determine if the animation has cycled on us
   S32 PrevNameIndex;			//Keeps PrevPos from cycling whenever we change threads in mid action state.
   bool Moving;					//quickly deterimine if we are a root animation
   U32 action;					//Which part of the root do we play

Initialize them to 0 in the TSThread() constructor
so in the same file and class we change
TSThread() {}
this into this
TSThread() {
   CurTrigger = 0;
   CurFrame = 0;
   CurShift = 0;
   PrevPos = 0;}

then in tsThread.cc
add to TSThread::TSThread(TSShapeInstance * _shapeInst)

CurTrigger = 0;
   CurFrame = 0;
   CurShift = 0;
   PrevPos = 0;


We also want to set these valuse to zero anytime the sequence changes so in
void TSThread::setSequence(S32 seq, F32 toPos) in the file tsThread.cc
add anywhere after the AssertFatal
CurTrigger = 0;
PrevPos = 0;
   CurFrame = 0;
   CurShift = 0;

While we are at it add into the enumeration in TSshape at the end
AnimListInit   = BIT(8),//defined for the new IFL type

This flag type will be quite handy later particularly in letting us know we should not be deleteing our gBitmap just yet

In order to tie this into regular animations a pointer to AnimList in the sequence struct also in TsShape is added

AnimList* AnimList;

Add this to the top of the Sequence Struct to make sure that AnimList is always NULL unless we are using it for somthing. There a few places in the code where we check this pointer to determine if we need to do anything. If it is not NULL it could crash say when loading a DSQ Datablock

Sequence(){AnimList = NULL;}

now to accutally make it animate we need to add again to tsShapeInstance.cc

inside the function
void TSShapeInstance::animate(S32 dl)
where
// animate ifl's?
   if (dirtyFlags & IflDirty)
      animateIfls();

Change it to this
// animate ifl's?
   if (dirtyFlags & IflDirty)
   {
      animateIfls();
	  animateSprites();
   }

Ok that should do it for the meat now for the bones loading our animation information from file.

at the top of TsShape.cc add the include
#include "sim/netStringTable.h"

The most heavily modified function and probably ugliest is the one that loads the modified IFL filetype in TsShape.cc
Change TSShape::readIflMaterials to this
void TSShape::readIflMaterials(const char* shapePath)
{
   if (mFlags & IflInit)
      return;
   //Con::printf("reading IFL file %s", shapePath);
   //Con::printf("mFlags = %u", mFlags);
   for (S32 i=0; i<iflMaterials.size(); i++)
   {
	   //Con::printf("we have a material to read"); 
      IflMaterial & iflMaterial = iflMaterials[i];

      iflMaterial.firstFrame = materialList->size(); // next material added will be our first frame
      iflMaterial.firstFrameOffTimeIndex = iflFrameOffTimes.size(); // next entry added will be our first
      iflMaterial.numFrames = 0; // we'll increment as we read the file

      U32 totalTime = 0;

      // Fix slashes that face the wrong way
      char * pName = (char*)getName(iflMaterial.nameIndex);
      S32 len = dStrlen(pName);
      for (S32 j=0; j<len; j++)
         if (pName[j]=='')
            pName[j]='/';

      // Open the file which should be located in the same directory
      // as the .dts shape.
      // Tg: I cut out a some backwards compatibilty code dealing
      // with the old file prefixing. If someone is having compatibilty
      // problems, you may want to check the previous version of this
      // file.

	  //Con::printf("pName %s", pName);
      char nameBuffer[256];
      dSprintf(nameBuffer,sizeof(nameBuffer),"%s/%s",shapePath,pName);

      Stream * s = ResourceManager->openStream(nameBuffer);
      if (!s || s->getStatus() != Stream::Ok)
      {
         iflMaterial.firstFrame = iflMaterial.materialSlot; // avoid over-runs
         if (s)
            ResourceManager->closeStream(s);
         continue;
      }

	  //Con::printf("nameBuffer %s", nameBuffer);
      // Parse the file, creat ifl "key" frames and append
      // texture names to the shape's material list.
      char buffer[512];
      U32 duration;
	  //Read First line of File and determine the type of IFL then let the appropiate code block run
	  if (s->getStatus() == Stream::Ok)
	  {
		  s->readLine((U8*)buffer,512);
		  if (s->getStatus() == Stream::Ok || s->getStatus() == Stream::EOS)
		  {
			  StringHandle Line(buffer);
			  if (Line == StringHandle("#IFL"))
			  {
				  //Con::printf("oh just old Code");
				  while (s->getStatus() == Stream::Ok)
				  {
					 s->readLine((U8*)buffer,512);
					 if (s->getStatus() == Stream::Ok || s->getStatus() == Stream::EOS)
					 {
						char * pos = buffer;
						while (*pos)
						{
						   if (*pos=='t')
							  *pos=' ';
						   pos++;
						}
						pos = buffer;

						// Find the name and duration start points
						while (*pos && *pos==' ')
						   pos++;
						char * pos2 = dStrchr(pos,' ');
						if (pos2)
						{
						   *pos2='�';
						   pos2++;
						   while (*pos2 && *pos2==' ')
							  pos2++;
						}

						// skip line with no material
						if (!*pos)
						   continue;

						// Extract frame duration
						duration = pos2 ? dAtoi(pos2) : 1;
						if (duration==0)
						   // just in case...
						   duration = 1;

						// Tg: I cut out a some backwards compatibilty code dealing
						// with the old file prefixing. If someone is having compatibilty
						// problems, you may want to check the previous version of this
						// file.
						// Strip off texture extension first.
						if ((pos2 = dStrchr(pos,'.')) != 0)
						   *pos2 = '�';
						//Con::printf("file name %s", pos);
						materialList->push_back(pos,TSMaterialList::IflFrame);
						//
						totalTime += duration;
						iflFrameOffTimes.push_back((1.0f/30.0f) * (F32)totalTime); // ifl units are frames (1 frame = 1/30 sec)
						iflMaterial.numFrames++;
					 }
				  }
			  }
			  else if (Line == StringHandle("#AnimList"))
			  {
				  //Con::printf("in animlist");
				  //checking for number of divisions then if that is good then we can move on to init stuff
				  s->readLine((U8*)buffer,512);
				  if (s->getStatus() == Stream::Ok || s->getStatus() == Stream::EOS)
				  {
					  //Our new IFL Type read the animation Frame data
					  mFlags |= AnimListInit;
					  // Strip off texture extension first.
					  char* dot = dStrchr(pName,'.');
					  if (dot != 0)
						  *dot = '�';
					  //we have removed the file extension now to use another dot to smuggle in the divisions of the file
					  //There should be NO . in the name at this point if there is good luck getting the divisions
					  char reBuffer[50];
					  //U8* Divisions;
					  //dSscanf(buffer, "%i", &Divisions);
					  //Con::printf("Buffer of Divisions %s", buffer);
					  dSprintf(reBuffer, 50, "%s.%s", pName, buffer);
					  //Con::printf("pName now %s", reBuffer);
					  materialList->push_back(reBuffer, TSMaterialList::Sprite);
					  //Con::printf("after push");
					  while (s->getStatus() == Stream::Ok)
					  {
						  s->readLine((U8*)buffer,512);
						  StringHandle String(buffer);
						  //gotta resize all the vectors to add a new sequence
						  //this->sequences.setSize(this->sequences.size() + 1);
						  //this->sequences[this->sequences.size() - 1].AnimList = new AnimList();
						  U8 SeqIndex = 0;
						  U8 NameIndex = 0;
						  const char* ret = String.getUnit(0, " tn");
						  char* Name = new char[dStrlen(ret)];
						  dStrncpy(Name, ret, dStrlen(ret));
						  Name[dStrlen(ret)] = '�';
						  //if their is an * anywhere in the name it will make it cyclic by default and truncate the name where it found the *
						  char* CycleChar = dStrchr(Name, '*');
						  bool TestCyclic = false;
						  if (CycleChar != NULL)
						  {
							  TestCyclic = true;
							  *CycleChar = '�';
						  }
						  
						  U8 count = 0;
						  bool found = false;
						  while (count < this->sequences.size())
						  {
							  //Con::printf("Comparing %s to %s", names[this->sequences[count].nameIndex], Name);
							  if (dStricmp(Name, names[this->sequences[count].nameIndex]) == 0)
							  {
								  //Con::printf("found the sequence yes!");
								  found = true;
								  SeqIndex = count;
								  NameIndex = this->sequences[count].nameIndex;
								  break;
							  }
							  count++;
						  }
						  if (found == false)
						  {
							  const Sequence* Seq = new Sequence();
							  this->sequences.insert(this->sequences.end(), *Seq);
							  SeqIndex = this->sequences.size() - 1;
							  this->names.insert(names.end(), Name);
							  NameIndex = this->names.size() - 1;
						  }
						  AnimList* List = new AnimList();
						  this->sequences[SeqIndex].AnimList = List;
						  this->sequences[SeqIndex].nameIndex = NameIndex;
						  this->sequences[SeqIndex].numKeyframes = String.getUnitCount(" tn") - 1;
						  if (TestCyclic)
						  this->sequences[SeqIndex].flags |= Cyclic;
						  this->sequences[SeqIndex].dirtyFlags = TSShapeInstance::IflDirty;
						  this->sequences[SeqIndex].duration = 0;
						  F32 AcumTime = 0;
						  U8 Count = 0;
						  U32 Frames = 0;
						  //Con::printf("numKeyframes we are counting to %i", this->sequences[SeqIndex].numKeyframes);
						  List->Trigger.setSize(0);
						  while (Count < this->sequences[SeqIndex].numKeyframes)
						  {
							  StringHandle CurFrame(String.getUnit(Count + 1, " tn"));
							  if (CurFrame.getString()[0] == '-')
							  {
								  const char* temp = CurFrame.getString();
								  temp++;
								  StringHandle trig(temp);
								  Con::printf("Temp %s", temp);
								  List->Trigger.setSize(Count+1);
								  List->Trigger[Count] = dAtoi(trig.getUnit(0, "-"));
								  List->TriggerNum.setSize(Count+1);
								  List->TriggerNum[Count] = dAtoi(trig.getUnit(1, "-"));
							  }
							  else
							  {
								  List->Duration.setSize(Frames+1);
								  List->Frame.setSize(Frames+1);
								  List->Frame[List->Frame.size() - 1] = dAtoi(CurFrame.getUnit(0, "/")) - 1;//easier to think casualy in terms of 1234 instead of 0123 for non programmers
								  List->Duration[List->Frame.size() - 1] = dAtof(CurFrame.getUnit(1, "/")) / 100;
								  this->sequences[SeqIndex].duration += List->Duration[List->Frame.size() - 1];
								  Frames++;
							  }
							  Count++;
						  }
						  StringHandle Str1(Name);
						  StringHandle Str2("Root");
						  if (Str1 == Str2)
						  {
							  //Con::printf("Found Root %s", Name);
							  this->sequences[SeqIndex].duration /= 8;
							  //Con::printf("Root Duration %f", this->sequences[SeqIndex].duration);
							  Count = 0;
							  List->KeyFrameTime.setSize(List->Frame.size());//we now know how big
							  U8 Limit = this->sequences[SeqIndex].numKeyframes / 8;
							  U8 AcuLimit = Limit;
							  //and again
							  while (Count < this->sequences[SeqIndex].numKeyframes)
							  {
								  if (Count == AcuLimit)
								  {
									  AcumTime = 0;
									  AcuLimit += Limit;
								  }
								  //Con::printf("AcuLimit %i and count %i", AcuLimit, Count);
								  //Con::printf("List->Duration[Count] %f / this->sequences[SeqIndex].duration %f", List->Duration[Count], this->sequences[SeqIndex].duration);
								  AcumTime += List->Duration[Count] / this->sequences[SeqIndex].duration;
								  
								  //Con::printf("AcumTime %f", AcumTime);
								  List->KeyFrameTime[Count] = AcumTime;
								  Count++;
							  }
						  }
						  else
						  {
							  //Con::printf("%s loaded with a duration of %f", Name, this->sequences[SeqIndex].duration);
							  Count = 0;
							  List->KeyFrameTime.setSize(List->Frame.size());//we now know how big
							  //and again
							  while (Count < this->sequences[SeqIndex].numKeyframes)
							  {
								  AcumTime += List->Duration[Count] / this->sequences[SeqIndex].duration;
								  //Con::printf("AcumTime %f", AcumTime);
								  List->KeyFrameTime[Count] = AcumTime;
								  Count++;
							  }
						  }
					  }
				  }
				  else
					  Con::errorf("improperly formated AnimeList file");
			  }
			  else
				  Con::errorf("non deliminated IFL.  Is it a regular IFL or AnimList?");
		  }
	  }
      // close the file...
      ResourceManager->closeStream(s);
   }
   initMaterialList();
   mFlags |= IflInit;
}

Also in tsShape.cc inside bool TSShape::read(Stream * s)
find
for (i=0; i<numSequences; i++)
      {
         constructInPlace(&sequences[i]);
         sequences[i].read(s);
      }
change that to
for (i=0; i<numSequences; i++)
      {
         constructInPlace(&sequences[i]);
         sequences[i].read(s);
		 sequences[i].AnimList = NULL;
      }

Now we can be sure AnimList is NULL when we check it if the object is not a sprite.

You might also notice i use StringHandles above in a manner they were probably not intended to. I am sure the code i copied over from the Getword console function is defined elsewhere and intended for what i used stringhandles for but i found them quite invaluable for parsing the file. I must say though they were written for script they are far more flexible than the script functions allow.

here is my modficiation to the StringHandle Class located in netStringTable.cc / .h

add to the declarations to the Stringhandle class in the .h
const char *getUnit(U32 index, const char *set);
   U32 getUnitCount(const char *set);

at the end of the .cc add

const char* StringHandle::getUnit(U32 i, const char *set)
	{
		const char *string = getString();
	   U32 sz;
	   while(i--)
	   {
		  if(!*string)
			 return "";
		  sz = dStrcspn(string, set);
		  if (string[sz] == 0)
			 return "";
		  string += (sz + 1);
	   }
	   sz = dStrcspn(string, set);
	   if (sz == 0)
		  return "";
	   char *ret = Con::getReturnBuffer(sz+1);

	   dStrncpy(ret, string, sz);
	   ret[sz] = '�';
	   return ret;
	}

U32 StringHandle::getUnitCount(const char *set)
	{
		const char *string = getString();
	   U32 count = 0;
	   U8 last = 0;
	   while(*string)
	   {
		  last = *string++;

		  for(U32 i =0; set[i]; i++)
		  {
			 if(last == set[i])
			 {
				count++;
				last = 0;
				break;
			 }
		  }
	   }
	   if(last)
		  count++;
	   return count;
	}

Now that the ugliest parts are over with we need to ensure our gBitmap is preserved when it's textureHandle is registered into the textureManager otherwise we will not be able to easily subdivide it.

Back in TsShapeInstance.cc in the function void TSShapeInstance::setMaterialList(TSMaterialList * ml)

find this line
mMaterialList->load(MeshTexture,hShape.getFilePath(),true);
and change it to
if (!(((TSShape*)mShape)->mFlags & ((TSShape*)mShape)->AnimListInit))
	  {
		  //Con::printf("load1");
		mMaterialList->load(MeshTexture,hShape.getFilePath(),true);
	  }
	  else//we need the texture for calculations
	  {
		  //Con::printf("load with BitmapSubdividedTexture flag");
		  mMaterialList->load(BitmapSubdividedTexture, hShape.getFilePath(), true);
	  }

here we use a new Enumeration value. This Value is quite handy at telling our code that we are not a IFL and that our gBitmap should be kept. in gTexManger.h the TextureHandleType Enumeration add a entry for
BitmapSubdividedTexture	  ///< Same as BitmapKeepTexture except it subdivides the texture into several textures used with AnimList animations

in tsMaterialList.cc
Change the void TSMaterialList::load(U32 index,const char* path) function to this

void TSMaterialList::load(U32 index,const char* path)
{
	//Con::printf("TsMaterialList Index %i getMaterialCount %i path %s", index, getMaterialCount(), path);
	//Con::printf("mTextureType is already set to %i", mTextureType);
	AssertFatal(index < getMaterialCount(),"TSMaterialList::getFlags: index out of range");
	if (mFlags[index] & TSMaterialList::Sprite)
	{
		//Con::printf("hooray in our own world");
		mTextureType = BitmapSubdividedTexture;
	}
	else if (mFlags[index] & TSMaterialList::NoMipMap)
   {
	   //Con::printf("we have a bitmapTexture");
      mTextureType = BitmapTexture;
   }
   else if (mFlags[index] & TSMaterialList::MipMap_ZeroBorder)
      mTextureType = ZeroBorderTexture;
   else
      mTextureType = MeshTexture;

   Parent::load(index,path);
}

in TsShape.h inside the class TSShape Find this Enumeration

enum
   {
      S_Wrap             = BIT(0),
      T_Wrap             = BIT(1),
      Translucent        = BIT(2),
      Additive           = BIT(3),
      Subtractive        = BIT(4),
      SelfIlluminating   = BIT(5),
      NeverEnvMap        = BIT(6),
      NoMipMap           = BIT(7),
      MipMap_ZeroBorder  = BIT(8),
      IflMaterial        = BIT(27),
      IflFrame           = BIT(28),
      DetailMapOnly      = BIT(29),
      BumpMapOnly        = BIT(30),
      ReflectanceMapOnly = BIT(31),
      AuxiliaryMap       = DetailMapOnly | BumpMapOnly | ReflectanceMapOnly
   };

and add to it

Sprite			 = BIT(9),


There are a whole bunch of places checks for BitmapSubdividedTexture needs to be checked. About anywhere BitmapKeepTexture gets checked in the TextureManager for the most part.

So in gTexManger.cc

in void TextureManager::resurrect()

where it says
if(probe->type == BitmapKeepTexture)
change to
if(probe->type == BitmapKeepTexture || probe->type == BitmapSubdividedTexture)

in void TextureManager::refresh(TextureObject *to)
where it says
if (to->type == BitmapTexture ||
       to->type == BitmapKeepTexture ||
       to->type == BitmapNoDownloadTexture
add into the if
|| to->type == BitmapSubdividedTexture

do the same in:
void TextureManager::refresh(TextureObject *to, GBitmap* bmp)
bool TextureManager::createGLName(GBitmap* pBitmap,
bool clampToEdge,
U32 firstMip,
TextureHandleType type,
TextureObject* to)
Add in a very similar fashion to two if statements of
bool TextureManager::createGLName(GBitmap* pBitmap,
bool clampToEdge,
U32 firstMip,
TextureHandleType type,
TextureObject* to)
making sure BitmapSubdividedTexture is treated much the same as BitmapKeepTexture

Modify three If's in the same manner in
TextureObject* TextureManager::registerTexture(const char* textureName, GBitmap* bmp, TextureHandleType type, bool clampToEdge)

now when we register our textureHandles the gbitmap should be preserved for us to subdivide.
Now again in gTexManager.h

Change bool TextureHandle::set(const char *textureName,
TextureHandleType type,
bool clampToEdge)
to
bool TextureHandle::set(const char *textureName,
          TextureHandleType type,
          bool clampToEdge)
{
	//Con::printf("called the modified set method");
   TextureObject* newObject;
   if (type == BitmapSubdividedTexture)
  {
	  //Con::printf("before new code");
	  StringHandle NameHandle(textureName);
	  StringHandle InterHandle(NameHandle.getUnit(1, "."));
	  U32 DivisionsX = dAtoi(InterHandle.getUnit(0, "*"));
	  U32 DivisionsY = dAtoi(InterHandle.getUnit(1, "*"));
	  const char* ret = NameHandle.getUnit(0, ".");
	  char* Name = new char[dStrlen(ret)];
	  dStrncpy(Name, ret, dStrlen(ret) + 1);
	  //Con::printf("after new code block");
	  //Con::printf("Divs XY %i %i", DivisionsX, DivisionsY);
	  //Con::printf("name %s textureName %s", Name, textureName);
	  newObject = TextureManager::loadTexture(Name, type, clampToEdge);
	  if (newObject != object)
	  {
		  unlock();
		  object = newObject;
		  subdivide(DivisionsX, DivisionsY, Name);
		  lock();
	  }
	  //Con::printf("before delete");
	  delete[] Name;
	  //Con::printf("after delete");
  }
  else
  {
	  newObject = TextureManager::loadTexture(textureName, type, clampToEdge);
	  if (newObject != object)
	  {
		  unlock();
		  object = newObject;
		  lock();
	  }
   }
   //Con::printf("before return");
   return (object != NULL);
}

one more thing till we can have our sprite play an animation via playthread

at the end of bool ShapeBase::onNewDataBlock(GameBaseData* dptr) in shapeBase.cc

add in
U8 Count1 = 0;
   if (mShapeInstance)
   if (mShapeInstance->mMaterialList)
   {
	   bool IsAnimList = false;
	   while (mShapeInstance->mMaterialList->mMaterials.size() > Count1)
	   {
		   if (mShapeInstance->mMaterialList->mMaterials[Count1].object)
		   if (mShapeInstance->mMaterialList->mMaterials[Count1].object->type == BitmapSubdividedTexture)
		   {
			   IsAnimList = true;
			   U8 Count2 = 0;
			   while (mShapeInstance->mShape->sequences.size() > Count2)
			   {
				   if (mShapeInstance->mShape->sequences[Count2].AnimList)
				   {
					   //might regret this next line later but for now
					   U8 Slot = mShapeInstance->mIflMaterialInstances[0].iflMaterial->materialSlot;
					   mShapeInstance->mShape->sequences[Count2].AnimList->MaterialSlot = Slot;
					   if (mShapeInstance->mMaterialList->mMaterialNames[Slot] == NULL)
					   {
						   //Con::printf("doing inital remap");
						   TextureHandle* Tex = mShapeInstance->mMaterialList->mMaterials.address();
						   TextureHandle* Temp = &Tex[Slot];
						   *Temp = Tex[Count1];
					   }
					   mShapeInstance->mShape->sequences[Count2].AnimList->mShape = mShapeInstance;
				   }
				   Count2++;
			   }
		   }
		   Count1++;
	   }
	   //good bye IFL Structure you are now in the way
	   if (IsAnimList == true)
		   mShapeInstance->mIflMaterialInstances.setSize(0);
   }

Unless i am missing some rather important piece of code you should be able to see a starting frame on your billboard charecters and be able to make it playthreads by command

in Part 2 i will get to creating a Sprite charecter class and integrating camera positioning and movement into playing our threads. Fortunatly that is a whole lot shorter Part 2

#1
10/12/2010 (12:20 pm)
Lovely!
#2
10/12/2010 (11:39 pm)
Nice :) I see the blog picture with the sprite, and what jumped to my head was ragnarok-type world in TGE.

Wow :) ok will give it a shot this weekend. :)
#3
10/13/2010 (7:57 am)
Yeah somthing pretty similar in looks to ragnarok is quite doable with an isometric orthographic view. That sprite though is acctually Laharl from Disgaea bit of a cult favorite game. Maybe i shoulda used etna or a prinny ;)

Before i go writting up part2 it seems pretty sensible to me to make sure that at least one person can follow my directions and get it to work as intended up to this point. So that I know I have not forgotten some important details.
#4
10/14/2010 (7:19 pm)
gratuitous video please
#5
10/15/2010 (2:20 pm)
Ok posted a video showing what works so far. Plus cleaned up the resource a little bit. What is typically used for a file server for resources on Garage Games. As i said at top never posted a resource before so not sure how to go about it. I think it would be alot easier to put up a download for at least a properly functioning DTS, Ifl and sprite sheet so that people can see if the code works first without adding the headache of trying to get the resource files working right too. Though i think i will have to find a non copyrighted sprite sheet to use for that.
#6
10/17/2010 (12:50 am)
I was wondering what code base you used when you made these changes?
#7
10/17/2010 (2:18 am)
oops i shoulda been alot more clear about that from the start i am using TGE 1.5.2 I am not all that familiar with the other code bases on account i do not own them. Though i have heard porting code between versions is not all that hard though I think the important things to watch for would be if they have the same or at least similar systems for IFL's and textures.

Oh i should mention i just finished putting up Part 2 had to make sure i did not break regular models and there control system and fixed a bug or two.
#8
11/09/2010 (6:40 pm)
woaaaaaaaaaa Fantastic, awesome!!!
#9
11/12/2010 (3:00 pm)
added in a little bit of an update and clearing up of some things. I plan to keep updateing this resource. As i find bugs and add to the code of it. Mind you this is a when i get around to it basis.
#10
11/17/2010 (7:08 am)
Great resource,thanks!
#11
02/05/2011 (5:31 am)
Well shoot seems it is 2000 words for blog posts. Seems i am for some reason at around 5800. So this kinda keeps me from doing any more updates on this particular resource post. However there is a rather nagging issue i cannot let stand on account of personal pride in my work.

at line 145 in the ReadiflMaterials I would strongly recomend changing the line to this:

char* Name = new char[dStrlen(ret)+1];

This prevents a hard to track array overflow caused by not accounting for the end string delimiter '0/'. Spent a couple weeks mucking about in the Torque Memory manager before I finally found it.

same deal with Line 14 of # bool TextureHandle::set(const char *textureName, TextureHandleType type, bool clampToEdge)

Change the line to this:
char* Name = new char[dStrlen(ret)+1];

oh FYI i think there may be one other problem with the code i have yet to find. Seem to on rare occasions get random crashes when first loading a map. Not sure if it is this code or some other wonky thing I have done. Just a heads up to keep your eyes open. If you find a problem it would help a lot.

If i ever get around re-posting a new version i will be sure to include those fixes in the code.