This proposal has been accepted
The development team has discussed the contents of this article and has decided that it should be implemented as described. But the implementation is not finished yet. You can help to bring the features described here into the game.
Each animation will be defined by an xml document, here follows an example:
<sprite name="player" action="stand"> <imageset name="base" src="graphics/sprites/player_male_base.png" width="64" height="64" /> <action name="stand" imageset="base"> <animation direction="down"> <frame index="0" /> </animation> <animation direction="left"> <frame index="18" /> </animation> <animation direction="up"> <frame index="36" /> </animation> <animation direction="right"> <frame index="54" /> </animation> </action> <action name="walk" imageset="base"> <animation direction="down"> <frame index="1" delay="50" /> <frame index="2" delay="50" /> <frame index="3" delay="50" /> <frame index="4" delay="50" /> <frame index="5" delay="50" /> <frame index="6" delay="50" /> </animation> <animation direction="left"> <sequence start="19" end="24" delay="50" /> </animation> <animation direction="up"> <sequence start="37" end="42" delay="50" /> </animation> <animation direction="right"> <sequence start="55" end="60" delay="50" /> </animation> </action> <action name="sit" imageset="base"> <animation direction="down"> <frame index="7" /> </animation> <animation direction="left"> <frame index="25" /> </animation> <animation direction="up"> <frame index="43" /> </animation> <animation direction="right"> <frame index="61" /> </animation> </action> <action name="dead" imageset="base"> <animation direction="down"> <frame index="8" /> </animation> <animation direction="left"> <frame index="26" /> </animation> <animation direction="up"> <frame index="44" /> </animation> <animation direction="right"> <frame index="62" /> </animation> </action> <action name="attack" imageset="base"> <animation direction="down"> <sequence start="9" end="12" delay="100" /> </animation> <animation direction="left"> <sequence start="27" end="30" delay="100" /> </animation> <animation direction="up"> <sequence start="45" end="48" delay="100" /> </animation> <animation direction="right"> <sequence start="63" end="66" delay="100" /> </animation> </action> </sprite>
So if you want to load the playerset you just load player.xml and it takes care of loading all related images. Of course delays are defined in milliseconds.
Specifications
<sprite>
A sprite is an object which can carry several animations, hence I call the root element the sprite
. Also, I think we should seperately define animations in several directions. For now this example is based on the current playerset image, but by using multiple imagesets this could of course be split up.
<action>
collection of the animation in different directions that belong to an action the character can perform. the properties are the imageset the animation phases are taken from and the name of the action.
possible names:
use-item | attack-bow cast-magic | | attack-stab attack-throw \ / run attack-swing sleeping dead | | | | walk attack sit hurt \ \ / / stand | (no name attribute)
this table also specifies the substitution precedence. when a specific name is requested but it is not defined the next lower name is used instead.
<animation>
Defines an animation sequence that should be displayed when the sprite object faces in a specific direction (attribute "direction").
the possible directions are:
up down left right
when a specific direction isn't provided, the first defined direction is used instead. this can be overridden by defining an animation with the direction "default" for this action.
Every <animation>
has one or more frame
or sequence
child elements.
<frame>
each of them defines one frame of the animation. the required properties are index
and optional offsetX
, offsetY
and delay
.
index
defines the index of the graphic on the spriteset. when a start
and a end tag is provided instead of an index
tag the
an optional property is delay
. every frame is displayed for delay
millisecounds and then replaced by the next frame in sequence. when the last frame is reached the sequence starts with the first frame again. when no delay is specified (or specified as "0") the animation doesn't continue when this frame
is reached.
Each frame
element can optionally have the properties offsetX
and offsetY
to specify an offset from the default drawing position for that frame. This allows the animation of for example the hairset (or any equipment) to reuse the same frames with different offsets.
<sequence>
sequence
tag does not only define one animation phase but multiple phases. Its properties are delay
, start
and end
which are all required. The delay is the delay in ms between each phase. the properties start
and end
define the first and the last index of the animation sequence on the spritesheet.
Suggested additions
multiple delays in a <frame> with start and end attribute
in the bow animation I've chosen a shortcut to specify different delays for different frames. I am undecided yet whether this is a nice feature or whether it'll be better to require multiple frame
elements in such cases.
<random> childtag for <animation>
Also, it includes an experimental suggestion on specifying that a random frame should be chosen.
sequences based on default sequences
After reflecting on this idea (and the above suggestion) I think a first issue to solve is to reduce the amount of duplicated definitions. For example, at the moment every monster uses the exact same frame size and animation frames. Hence, it would be interesting if the defined sprite could take the actual imageset it uses as a parameter instead of specifying this on its own. This way we can define a monster like:
<sprite name="monster-walking-default"> <imageset name="base" /> ... </sprite> <being name="Scorpion"> <sprite ref="monster-walking-default"> <with-imageset name="base" src="scorpion-base.png" width="60" height="60" /> </sprite> </being> <being name="Red Scorpion"> <sprite ref="monster-walking-default"> <with-imageset name="base" src="scorpion-red-base.png" width="60" height="60" /> </sprite> </being>
Defining an empty imageset (without src
attribute) will make the engine require this imageset to be passed as a parameter whenever the spriteset is referenced.
I'm not convinced yet that the name
attribute of the sprite
element is really necessary. I think I would prefer each sprite to be defined in its own file. However it could be attractive to define multiple things in the same file, but in that case I think we will have to read everything when the client is launched, and only graphics and imagesets can be created lazily (only when necessary). Because the other data consists of just numbers and strings, this should not be really a problem.
An implementation idea by Peoro
An idea to manage the animations should be a class based on a timer which choose automatically the current frame. Who needs animations (players, monsters and npcs) has only to start the animanion and to request the frame to draw it. Each instance of Animation class keep its own start-time (or a personal timer), its total time, number of frames, and the time of every frame. The Animation class has to provide at least a function to start the animation and another one to get the current frame; other usefull functions should pause and restart the animation and allow to jump to a random frame.
Here a simple example of a prototype of an Animation class: (I mutilated my original Animation class to simplify and summarize it cutting a lot of other functions and vars and using the most possible of standard items)
class Animation { int nframes; Img **frames; int *times; int totalTime; int startTime; public: Animation ( int n, Img **i, int *t ) { int c; nframes = n; frames = new Img* [n]; times = new int [n]; totalTime = 0; for ( c = 0; c < n; c ++ ) { frames[c] = i[c]; times[c] = t[c]; totalTime += t[c]; } } inline void start ( int t = clock() ) { startTime = t; } Img *getFrame ( int t = clock() ) { int i; int it if ( ! totalTime ) { t = 0; } else { t = (t-startTime) % totalTime; } // searches the current frame for ( i = 0, it = 0; (it + times[i]) < t; i ++, it += times[i] ); return frames[i]; } };
And here how to use this class:
Img * imgs [] = { img1, img2, img3 }; int times [] = { 20, 20, 30 }; Animation a ( 3, imgs, times ); a.start ( ); while ( 1 ) { draw ( a.getFrame() ); }
An advantage of this class is that the animation speed is indipendent to FPS and it's very easy to use.
This is a simple way to manage a lot of animations for who needs more than one:
class AnimationsList { // ltstr is only a const char* comparison function map<const char*, Animation*, ltstr> animations; int startTime; // now Animation::startTime is no more usefull Animation *currentAnimation; public: AnimationsList ( ) : currentAnimation(0) { } inline void addAnimation ( const char *n, Animation *a ) { animations[n] = a; } int setAnimation ( const char *n, int t = clock() ) { map<const char*, Animation*, ltstr>::iterator ai; Animation *a; ai = animations.find ( n ); if ( ai == animations.end() ) { // error... animation not found return 0; } a = ai->second; if ( currentAnimation != a ) { a->start ( ); currentAnimation = a; startTime = t; } return 1; } Img * getFrame ( ) { if ( currentAnimation ) { return currentAnimation->getFrame ( ); } return 0; } };
Using a multimap instead the map it's possible to keep more than an animation bind to the same string. This would be a good way to make AnimationsList choose a random animation.
And here how to use AnimationsList class:
AnimationsList al; al.addAnimation ( "walking" "south", anim1 ); al.addAnimation ( "walking" "north", anim2 ); al.addAnimation ( "anim3", anim3 ); al.setAnimation ( "walking" "south" ); while ( 1 ) { draw ( al.getFrame() ); }
I hope not to had make a mistake writing... Anyway these are only theorical bases.
Implementation proposal by Doener
This proposal is incomplete regarding how new sequences are added to the Animation (which is not the right name for this type of class *g*)... Anyway, you should get the idea... The Frame class encapsulates images and are used to create a single linked list that creates an animation. The idea behind the linked list is that you can easily create loops, by having the last element of the animation pointing to the first one. Also, you can easily prepend a "starting animation" before the loop, like in an animation of a running player. He starts out with one animation and then loops the real running... The Animation class is responsible for mapping animation names to their starting frame and for disposing the encapsulating Frame objects, thus a std::map and a std::list is used to keep track of those objects. The next frame is simply calculated from the time that has passed since the last run of the logic method. The case that a whole animation loop can be skipped is not taken into consideration, because a) we cannot do this with frames linked together arbitrarily and b) animations will probably take around 100ms or longer, ie. we'd have less than 10fps for that case to happen and then there's nothing left to gain by that simple optimization anymore anyway ;)
For the linked list features, of course the xml format needs to be adjusted to support that.
/* * Frame class, stored information about frames in an animation */ struct Frame { Image *mImg; // Image for this frame int mDelay; // Time in ms for how long this frame is shown Frame *mNext; // Next frame in sequence }; class Animation { public: Animation():mCurrentFrame(0); ~Animation(); void setSequence(const std::string &); void logic(int timePassed); Image* getCurrentImage() { return mCurrentFrame ? mCurrentFrame->mImg : 0; } private: Frame *mCurrentFrame; typedef std::map<std::string,Frame*> FrameMap; typedef FrameMap::iterator FrameMapIterator; FrameMap mStartFrames; std::list<Frame*> mFrames; int mRemain; } Animation::~Animation() { for_each(mFrames.begin(), mFrames.end(), make_dtor(mFrames)); } void Animation::setSequence(const std::string &name) { FrameMapIterator i = mStartFrames.find(name); mCurrentFrame = (i == mStartFrames.end()) ? 0 : i->second; mRemain = 0; } void Animation::logic(int timePassed) { mRemain += timePassed; while (mRemain >= mCurrentFrame->mDelay) { mRemain -= mCurrentFrame->mDelay; mCurrentFrame = mCurrentFrame->mNext; } };