Playing part of an animation xml file

I have this sprite image xml file and want to play a range of it:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with TexturePacker https://www.codeandweb.com/texturepacker -->
<!-- $TexturePacker:SmartUpdate:0c3d3ab755d2dfd798e1fa5e54f48bbc:131458e89c5c6c5178fb57a8104bd8ec:9a06091b55b8b7c29d9674316a7dcf4c$ -->
<TextureAtlas imagePath="kyleyturn.png" width="8677" height="1204">
    <SubTexture name="kyleyturn_01" x="1" y="1" width="303" height="1182" frameX="-40" frameY="-12" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_02" x="306" y="1" width="299" height="1182" frameX="-44" frameY="-14" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_03" x="607" y="1" width="286" height="1178" frameX="-55" frameY="-23" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_04" x="895" y="1" width="276" height="1180" frameX="-71" frameY="-25" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_05" x="1173" y="1" width="257" height="1185" frameX="-93" frameY="-22" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_06" x="1432" y="1" width="244" height="1184" frameX="-105" frameY="-24" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_07" x="1678" y="1" width="217" height="1181" frameX="-129" frameY="-28" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_08" x="1897" y="1" width="189" height="1178" frameX="-147" frameY="-30" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_09" x="2088" y="1" width="205" height="1174" frameX="-141" frameY="-32" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_10" x="2295" y="1" width="192" height="1195" frameX="-125" frameY="-18" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_11" x="2489" y="1" width="206" height="1192" frameX="-109" frameY="-19" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_12" x="2697" y="1" width="213" height="1191" frameX="-101" frameY="-20" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_13" x="2912" y="1" width="268" height="1192" frameX="-64" frameY="-20" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_14" x="3182" y="1" width="293" height="1192" frameX="-52" frameY="-21" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_15" x="3477" y="1" width="315" height="1190" frameX="-45" frameY="-22" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_16" x="3794" y="1" width="336" height="1188" frameX="-41" frameY="-22" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_17" x="4132" y="1" width="348" height="1183" frameX="-44" frameY="-23" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_18" x="4482" y="1" width="366" height="1184" frameX="-34" frameY="-23" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_19" x="4850" y="1" width="357" height="1187" frameX="-43" frameY="-24" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_20" x="5209" y="1" width="341" height="1188" frameX="-56" frameY="-24" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_21" x="5552" y="1" width="304" height="1186" frameX="-73" frameY="-26" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_22" x="5858" y="1" width="254" height="1184" frameX="-93" frameY="-26" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_23" x="6114" y="1" width="209" height="1184" frameX="-102" frameY="-27" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_24" x="6325" y="1" width="192" height="1186" frameX="-108" frameY="-26" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_25" x="6519" y="1" width="212" height="1202" frameX="-127" frameY="-10" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_26" x="6733" y="1" width="227" height="1202" frameX="-116" frameY="-10" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_27" x="6962" y="1" width="247" height="1202" frameX="-97" frameY="-10" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_28" x="7211" y="1" width="274" height="1201" frameX="-68" frameY="-10" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_29" x="7487" y="1" width="278" height="1201" frameX="-63" frameY="-10" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_30" x="7767" y="1" width="291" height="1197" frameX="-45" frameY="-11" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_31" x="8060" y="1" width="304" height="1191" frameX="-35" frameY="-13" frameWidth="400" frameHeight="1260"/>
    <SubTexture name="kyleyturn_32" x="8366" y="1" width="310" height="1183" frameX="-32" frameY="-16" frameWidth="400" frameHeight="1260"/>
</TextureAtlas>

Then I load it with:

Player.TurnScene.URL:= ‘kyleyturn.starling-xml#fps:10,anim-naming:strict-underscore’;

If I try to play the first 4 images with:
PlayAnimation(‘kyleyturn_01, kyleyturn_02, kyleyturn_03, kyleyturn_04’, false);

it does not work (it only shows the first image)

If I use:
PlayAnimation(‘kyleyturn’, false);
it plays all 32 images.

But in this example I want to play just images 1-4. So how can I just play a range?
I read https://github.com/castle-engine/castle-engine/wiki/Sprite-sheets and cannot find what is wrong.

PlayAnimation takes an animation name, not a list of frame names.

If you want to have an animation that is a subset of another animation, right now you just have to define the new animation, as kyleyturnShort or something like that.

It’s simplest to use of CGE Sprite Sheet Editor to copy the old animation to new, and remove unneeded frames from the new animation.

The idea is that the sprite character can turn in every direction.

I have now renamed the first subtexture names to:

kyleyturnfronttosw_01
kyleyturnfronttosw_02
kyleyturnfronttosw_03
kyleyturnfronttosw_04

(turns facing front to facing south west)

PlayAnimation(‘kyleyturnfronttosw’, false);

This works, but if I, for instance, want to turn in one animation sequence from facing front to back I would need 4 different lists that play after each other.
Or is there a way to set a Playanimation list?

PlayAnimation(‘kyleyturnfronttosw’, false); // (turns facing front to facing south west)
PlayAnimation(‘kyleyturnswtoleft’, false); // (turns facing south west to facing left)
PlayAnimation(‘kyleyturnlefttonw’, false); // (turns facing left to north west)
PlayAnimation(‘kyleyturnnwtoback’, false); // (turns facing north west to back)

This only plays the last sequence. :face_with_raised_eyebrow:
So every next Playanimation command should “wait” for the previous one to finish, but I don’t know how.

I noticed the backlog in the new sprite editor and found:

image

This looks interesting because I would be able to create different xml files for the same .png, so using different anim names for the same images and so different subsets.
But the sprite editor development looks quiet for some Months and is not updated since 21 April.

Use TPlayAnimationParameters.StopNotification feature to implement “playing a list of animations, in a sequence”.

This mechanism was already mentioned, with examples, in 2 threads where you participated :slight_smile:

  1. Sprite control with the new system - #2 by michalis ,

  2. Play all animations in sequence - #8 by michalis .

The latter thread contains a ready example from me that you can adjust to do exactly what you need here. It implements RunAllAnimations , taking AnimList := Scene.AnimationsList. You can adjust it to implement instead

RunAnimationList(const Scene: TCastleSceneCore; const AnimNames: array of Strings)

You would then call it like

RunAnimationList(Scene, ['kyleyturnfronttosw’,‘kyleyturnswtoleft’, ‘kyleyturnlefttonw’, ‘kyleyturnnwtoback'])

The only difference between your RunAnimationList and the RunAllAnimations is how to populate the AnimList, list of animations scheduled to play. In RunAnimationList you should

  • make sure to clear AnimList and stop current animation, if any
  • populate the AnimList with new animation names you want to play (``AnimNames` from my declaration)

You can already do this. You can refer to the same PNG image from multiple sprite sheets, in any format (Starling XML or .castle-sprite-sheets), this was never a problem. You can just edit the XML files in a text editor to make them refer to the same PNG.

The “Support for multile atlas files for one sprite sheet” ticket you mention is about a different thing – to have one .castle-sprite-sheet refer to multiple PNG files.

Because the main features are working and we are just using them, see e.g. recent news about Platformer by Castle Game Engine release – it’s a CGE demo that is using new sprite sheets for everything.

Sorry, I already had a deja-vu about this :wink:
Thanks. I still find it difficult to implement.
I also looked at the examples\animations\play_animation.
I tried out to start as simple as possible and now have:

 procedure CreateAnimationSequence;
  var
    AnimationName: String;
   begin
     Player.TurnScene.AnimationsList.LoadFromURL('kyleyturn.starling-xml#fps:10,anim-naming:strict-underscore');
     for AnimationName in Player.TurnScene.AnimationsList do
     begin
      RunAnimationList(Player.TurnScene, ['kyleyturnfronttosw’,‘kyleyturnswtoleft’, ‘kyleyturnlefttonw’, ‘kyleyturnnwtoback'])
  end;  

Is this okay to load the existing animations from xml file and select a few of them with the (still not existing RunAnimation procedure)?

Sorry, I don’t follow what your code above tries to do.

  • You should load the scene from a sprite sheet. There is no AnimationsList.LoadFromURL.

  • You should play all animations with one call to RunAnimationList(Player.TurnScene, 'kyleyturnfronttosw’,‘kyle....). There’s no point for a loop there (for AnimationName in Player.TurnScene.AnimationsList do...).

My advise:

  • create a new project, with a viewport with 2D camera, that only contains one scene with a sprite sheet.

  • Try to adjust my code from Play all animations in sequence - #8 by michalis to implement RunAllAnimations in that new simple project. See that it works.

  • Then remake it to implement RunAnimationList as I described above.

Ok, I tried to create a new project with the CGE template but I noticed the properties do not show up anymore in Lazarus. Then I re-installed lazarus and latest CGE but I cannot access the properties anymore(I tried the standard LabelFps).
Loading CGE examples and trying to change properties in code also do not show up (though they work, I tried changing label caption and it shows my edited text when I compile from the editor.
Any idea what is wrong here?

@michalis
Also I am trying to use your:

I got an error, though Animlist is a TStringlist it says it is a TString.

I have put all stuff in the attachment (and commented out some procedures as I get errors).
Can you please take a look at it? This is difficult for me.

animationsequence3.zip (61.0 MB)

Maybe you didn’t open the proper Lazarus project (and only a file in this project)? Use “Project → Open Project” in Lazarus to open the right project. Or use “Code → Open Project In Code Editor” from CGE editor.

It’s easy right now to mistakenly forget about it, because of Trello

TStrings is more abstract ancestor of TStringList. You probably need to change Animlist to be just TStrings as well in this example.

I recommend first learning these classes, if you don’t know them. Create a new project, unrelated to CGE, and play there with creating, copying contents of etc. of TStringList. See docs like

Yes, that did it. Thanks.

Yes, when I change AnimList: TStringList to
AnimList: TStrings I get no errors. Makes sense because Scene property AnimationsList is also a TString.

Clipboard01

Now I get TTimeSensorNode error.
I have uploaded a new attachment. I just need to play the sequence of the 4 animation names that are in the sprite xml screen. Can you please help me with the code? I got my game animation stuff almost complete except playing sequences of animations.

animationsequence4.zip (44.3 MB)

The first step was to make RunAllAnimations running, so that my example from Play all animations in sequence - #8 by michalis works.

Please first make sure it works – that you have a working RunAllAnimations, in a simple test application.

Try to understand that code. Code from the outside should not call RunNextAnimation, this doesn’t make sense – RunNextAnimation is just a helper method, we need it as a the Params.StopNotification callback. Code from the outside should only call RunAllAnimations. I emphasize: make sure it works, and play with this code so you understand what it does.

Once you got it, the next step is to change RunAllAnimations into something like RunAnimationSequence . It should take, as arguments, the Scene, and the list of animation names, and look like

procedure xxx.RunAnimationSequence(const Scene: TCastleSceneCore; const Names: array of String);
begin
  ..// implementation should be similar to RunAllAnimations, but you have to initialize new AnimList with the contents of Names
end;

The only thing you have to do is initialize AnimList to be

  • a new instance of TStringList
  • to contain a copy of Names: array of String

Ok, I made the code as small as possible and focused on procedure RunAllAnimation.
Now I get an access violation error.

Clipboard01

I thought Scene1.AnimationsList automatically contains all animation names of the loaded “kyleyturn.starling-xml” file?

unit GameStateMain;

interface


uses Classes,
  CastleUIState, CastleComponentSerialize, CastleUIControls, CastleControls,
  CastleKeysMouse, CastleViewport, CastleScene, CastleVectors, StrUtils, Dialogs,
  CastleWindow,CastleUtils, CastleGLImages, CastleFilesUtils,
  CastleSoundEngine, CastleTimeUtils, CastleColors,
  CastleRectangles, CastleFonts, CastleGLUtils, Generics.Defaults,
  Generics.Collections, CastleDownload, CastleSceneCore,
  CastleCameras, CastleTransform, CastleImages, X3DNodes, X3DFields, CastleURIUtils;


type
  TStateMain = class(TUIState)
  private
    Viewport: TCastleViewport;
   public
    constructor Create(AOwner: TComponent); override;
    procedure Start; override;
  end;

  type
  TSomeClass = class
  private
    AnimList: TStrings;
    NextAnim: Integer;
    procedure RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
  public
    procedure RunAllAnimations(const Scene: TCastleSceneCore);
  end;


var
  StateMain: TStateMain;
  Scene1: TCastleScene;
  PlayList: TSomeClass;

implementation

uses SysUtils;

{ TStateMain ----------------------------------------------------------------- }

constructor TStateMain.Create(AOwner: TComponent);
begin
  inherited;
  DesignUrl := 'castle-data:/gamestatemain.castle-user-interface';
end;

procedure TStateMain.Start;
begin
  inherited;

  Viewport := DesignedComponent ('Viewport') as TCastleViewport;

  Scene1 := DesignedComponent('Scene') as TCastleScene;
  Viewport.Items.Add(Scene1);

  Scene1.Translation:= Vector3(800,300,0);
  Scene1.URL:= 'castle-data:/kyleyturn.starling-xml';

  PlayList.RunAllAnimations(Scene1);

 end;

procedure TSomeClass.RunAllAnimations(const Scene: TCastleSceneCore);
begin
  AnimList := Scene.AnimationsList;
  NextAnim := 0;
  RunNextAnimation(Scene, nil);
end;

procedure TSomeClass.RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
var
  NextAnimationName: String;
  Params: TPlayAnimationParameters;
begin
  NextAnimationName := AnimList[NextAnim];
  NextAnim := (NextAnim + 1) mod AnimList.Count;

  Params := TPlayAnimationParameters.Create;
  try
    Params.Name := NextAnimationName;
    Params.StopNotification := @RunNextAnimation;
    Scene.PlayAnimation(Params);
  finally FreeAndNil(Params) end;
end;


end.

Looking at your code: you declare PlayList to be an instance of class TSomeClass, but you never actually create it, you never call the TSomeClass constructor. That’s the reason for Access Violation – your code cannot write to the field AnimList, because it is part of PlayList instance, but PlayList was never created.

If you’re not sure how to create an instance of the class, consult learning resources about Pascal, like

Note that you could also just place the example code from Play all animations in sequence - #8 by michalis inside the state class, i.e. you don’t really need a new class (TSomeClass), you can just use the TStateMain class and extend it.

That is what my comment in that example meant:

TSomeClass = class // place this in any class you have handy

Ah, of course, stupid of me. Normally I get a SIGSEGV error when I forget to create the instance.
Sorry!

Yes, I finally got RunAllAnimations working, thanks! All animations are playing now.

Now I added a RunAnimationList procedure (and outcommented the RunAllAnimations for now) but it is not quite clear to me how to load the 4 animation strings in the start procedure.

Yes, I don’t know how to put this in the RunAnimationList procedure now, sorry.
And then they only have to play once in the RunNext procedure, so they should not loop.


unit GameStateMain;

interface


uses Classes,
  CastleUIState, CastleComponentSerialize, CastleUIControls, CastleControls,
  CastleKeysMouse, CastleViewport, CastleScene, CastleVectors, StrUtils, Dialogs,
  CastleWindow,CastleUtils, CastleGLImages, CastleFilesUtils,
  CastleSoundEngine, CastleTimeUtils, CastleColors,
  CastleRectangles, CastleFonts, CastleGLUtils, Generics.Defaults,
  Generics.Collections, CastleDownload, CastleSceneCore,
  CastleCameras, CastleTransform, CastleImages, X3DNodes, X3DFields, CastleURIUtils;


type
  TStateMain = class(TUIState)
  private
    Viewport: TCastleViewport;
    AnimList: TStrings;
    NextAnim: Integer;
    procedure RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
   public
    constructor Create(AOwner: TComponent); override;
    procedure Start; override;
    procedure RunAllAnimations(const Scene: TCastleSceneCore);
    procedure RunAnimationList(const Scene: TCastleSceneCore; const AnimNames: array of String);
  end;


var
  StateMain: TStateMain;
  Scene1: TCastleScene;

implementation

uses SysUtils;

{ TStateMain ----------------------------------------------------------------- }

constructor TStateMain.Create(AOwner: TComponent);
begin
  inherited;
  DesignUrl := 'castle-data:/gamestatemain.castle-user-interface';
end;

procedure TStateMain.Start;
begin
  inherited;

  Viewport := DesignedComponent ('Viewport') as TCastleViewport;

  Scene1 := DesignedComponent('Scene') as TCastleScene;
  Viewport.Items.Add(Scene1);

  Scene1.Translation:= Vector3(800,300,0);
  Scene1.URL:= 'castle-data:/kyleyturn.starling-xml';

  // RunAllAnimations(Scene1);
  RunAnimationList(Scene1, ['kyleyturnfronttosw’,‘kyleyturnswtoleft’, ‘kyleyturnlefttonw’, ‘kyleyturnnwtoback']);

 end;

procedure TStateMain.RunAllAnimations(const Scene: TCastleSceneCore);
begin
//  AnimList := Scene.AnimationsList;
//  NextAnim := 0;
//  RunNextAnimation(Scene, nil);
end;

procedure TStateMain.RunAnimationList(const Scene: TCastleSceneCore; const AnimNames: array of String);
begin
  AnimList := ??????????????
  NextAnim := 0;
  RunNextAnimation(Scene, nil);
end;


procedure TStateMain.RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
var
  NextAnimationName: String;
  Params: TPlayAnimationParameters;
begin
  NextAnimationName := AnimList[NextAnim];
  NextAnim := (NextAnim + 1) mod AnimList.Count;

  Params := TPlayAnimationParameters.Create;
  try
    Params.Name := NextAnimationName;
    Params.StopNotification := @RunNextAnimation;
    Scene.PlayAnimation(Params);
  finally FreeAndNil(Params) end;
end;


end.

Please read about and learn to use TStringList class, standard in FPC and Delphi. I provided links to their documentation earlier in this thread:

What you need in this case is to create new TStringList instance (or clear some existing one) and add there all strings from the array that RunAnimationSequence gets as an argument (like const Names: array of String).

You can also use our TCastleStringList (see docs Castle Game Engine: CastleStringUtils: Class TCastleStringList ), that descends from TStringList and adds some helpers like AddRange.

To not loop, modify the NextAnim variable behavior. Right now it’s incremented such that it loops:

NextAnim := (NextAnim + 1) mod AnimList.Count;

You want to instead increment it simply, and do not call next animation if NextAnim reached AnimList.Count.