Playing part of an animation xml file

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.

Thanks for the links but I did not find examples in them. A summary of all functions and properties to me is like showing me all parts of a car without telling me how to actually drive the thing.
This is what I miss: examples and tutorials. :slight_smile:
The Delphi boot camp course looks good though as it shows tutorial like. I will start to learn this route.
But I have found a few examples on TStringList and this actually works:


type
  TStateMain = class(TUIState)
  private
    Viewport: TCastleViewport;
    AnimList: TStrings;
    SequenceList: TStringList;
    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; 

procedure TStateMain.RunAnimationList(const Scene: TCastleSceneCore; const AnimNames: array of String);
begin
  SequenceList.Add('kyleyturnfronttosw');
  SequenceList.Add('kyleyturnswtoleft');
  SequenceList.Add('kyleyturnlefttonw');
  SequenceList.Add('kyleyturnnwtoback');

  AnimList := SequenceList;
  NextAnim := 0;
  RunNextAnimation(Scene, nil);
end;                         

So I am already happpy with that.
Next thing I tried is putting the arguments of

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

instead of the list that I added in RunAnimationList
as argument so I had to use ‘const AnimNames: array of String’

SequenceList.Add(AnimNames[1]);
SequenceList.Add(AnimNames[2]);
SequenceList.Add(AnimNames[3]);
SequenceList.Add(AnimNames[4]);

This is of course not right, but how can I get the [‘kyleyturnfronttosw’,‘kyleyturnswtoleft’, ‘kyleyturnlefttonw’, ‘kyleyturnnwtoback’]); list into it? I cannot find any examples on this.

If I make it:

if NextAnim = AnimList.Count then Exit else Inc(NextAnim); 

I get a ‘list out of bounds’ error and if I make it

if NextAnim = AnimList.Count-1 then Exit else Inc(NextAnim);

it exits the procedure after playing the third animation (and does not play the last animation?)

There are lots of examples of TStringList, I see you found some of them. I found https://wiki.lazarus.freepascal.org/TStringList-TStrings_Tutorial .

The Names is an array of Strings. More precisely, an “open array” that is passed as an argument. The first string has index 0, last string has index High(Names).

You need to learn how to iterate over an array of strings now. For each item in Names, you want to do SequenceList.Add(Names[I]).

You need to place a check for this at a proper place of the application. Find the right place to put

Inc(NextAnim); 

And find the right place to put exit condition:

if NextAnim = AnimList.Count then Exit;

They may not have to be done together. Track the value of NextAnim variable to understand when it gets large enough to stop the iteration.

I got a “list out of bounds” as soon AnimNames[I] was 1 or higher. Only 0 worked. So it seemed there was only 1 string in the array.
Then I tried separating the strings in the array again and found out the line
[‘kyleyturnfronttosw’,'kyleyturnswtoleft’, ‘kyleyturnlefttonw’, ‘kyleyturnnwtoback’] was wrong.
I had copied it from your example but some commas were the wrong characters.
It had to be [‘kyleyturnfronttosw’,‘kyleyturnswtoleft’, ‘kyleyturnlefttonw’, 'kyleyturnnwtoback’].
:slight_smile:
In the forum the commas do not show up right, as I noticed when I “corrected” them. They are replaced by accolades.

Solved.

And here is the final code. I don’t know if this is the best method but it works like a charm. :slight_smile:
I added a PlayForward switch so the order of playing animation of the sprite screen can also be from last to first or something in between, so the turning of the sprite can be clockwise or counterclockwise.


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;
    SequenceList: TStringList;
    NextAnim: Integer;
    PlayForward: Boolean;
    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:/kyley_idle.starling-xml';

  SequenceList := TStringList.Create();

 // RunAllAnimations(Scene1);

  PlayForward := False;
  RunAnimationList(Scene1, ['kyleyfront', 'kyleyturnsw', 'kyleysw', 'kyleyturnleft', 'kyleyleft', 'kyleyturnnw', 'kyleynw', 
  'kyleyturnback', 'kyleyback', 'kyleyturnne', 'kyleyne', 'kyleyturnright', 'kyleyright', 'kyleyturnse', 'kyleyse', 'kyleyturnfront', 'kyleyfront']);
 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);
var I: integer;
begin
  if PlayForward then
  begin
    For I := 0 to High(AnimNames) do
    begin
      SequenceList.Add(AnimNames[I]);
    end;
  end else
  begin
    For I := High(AnimNames) downto 0 do
    begin
    SequenceList.Add(AnimNames[I]);
  end;
end;

  AnimList := SequenceList;
  NextAnim := 0;
  RunNextAnimation(Scene, nil);
end;


procedure TStateMain.RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
var
  NextAnimationName: String;
  Params: TPlayAnimationParameters;
begin
  if NextAnim = AnimList.Count then
  begin
    FreeAndNil(SequenceList);
    Exit;
  end;

  NextAnimationName := AnimList[NextAnim];
  Inc(NextAnim);

  Params := TPlayAnimationParameters.Create;
  try
    Params.Name := NextAnimationName;
    Params.Forward:= PlayForward;
    Params.StopNotification := @RunNextAnimation;
    Scene.PlayAnimation(Params);

  finally FreeAndNil(Params) end;
end;

end.
1 Like

Now I edited it somewhat.
What it should do is

  1. play the animations up until the selected LastAnimationName and then exit. (this works okay).
  2. Then by pushing a button it should start animation from the last animation (which is the current animation) and end with the animation that is declared in the push button procedure.
    I cannot get this to work; currently every animation sequence starts from the first animation (standfront).
    If I start with NextAnim = FirstAnim (which is 8, the number it should start with (StandBack) it does not work.
    I would like some help here. Thanks.
    (see attachment for the code and data)
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;
    SequenceList: TStringList;
    FirstAnim: Integer;
    NextAnim: Integer;
    LastAnim: Boolean;
    FirstAnimationName, LastAnimationName: String;
    PlayForward: Boolean;
    procedure StandLeft(Sender: TObject);
    procedure StandNE(Sender: TObject);

    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;
  Button1, Button2: TCastleButton;
  Label1, Label2: TCastleLabel;
  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;

  Button1 := DesignedComponent('Button1') as TCastleButton;
  Button2 := DesignedComponent('Button2') as TCastleButton;
  Label1 := DesignedComponent('Label1') as TCastleLabel;
  Label2 := DesignedComponent('Label2') as TCastleLabel;

  Button1.Caption := 'standleft';
  Button2.Caption := 'standne';

  Button1.OnClick:= @StandLeft;
  Button2.OnClick:= @StandNE;

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

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

  SequenceList := TStringList.Create();



 // RunAllAnimations(Scene1);


//  RunAnimationList(Scene1, ['kyleyfront', 'kyleyturnsw', 'kyleysw', 'kyleyturnleft', 'kyleyleft', 'kyleyturnnw', 'kyleynw',
 // 'kyleyturnback', 'kyleyback', 'kyleyturnne', 'kyleyne', 'kyleyturnright', 'kyleyright', 'kyleyturnse', 'kyleyse', 'kyleyturnfront', 'kyleyfront']);

 FirstAnimationName := 'johnstandfront';
 LastAnimationName := 'johnstandback';
 PlayForward := True;

 RunAnimationList(Scene1, ['johnstandfront', 'johnturnsw', 'johnstandsw', 'johnturnleft', 'johnstandleft', 'johnturnnw', 'johnstandnw', 'johnturnback', 'johnstandback',
 'johnturnne', 'johnstandne', 'johnturnright', 'johnstandright', 'johnturnse', 'johnstandse', 'johnturnfront', 'johnstandfront']);

 end;

procedure TStateMain.StandLeft(Sender: TObject);
begin
  SequenceList := TStringList.Create();
  LastAnimationName := 'johnstandleft';
  PlayForward := False;
  RunAnimationList(Scene1, ['johnstandfront', 'johnturnsw', 'johnstandsw', 'johnturnleft', 'johnstandleft', 'johnturnnw', 'johnstandnw', 'johnturnback', 'johnstandback',
 'johnturnne', 'johnstandne', 'johnturnright', 'johnstandright', 'johnturnse', 'johnstandse', 'johnturnfront', 'johnstandfront']);
end;

procedure TStateMain.StandNE(Sender: TObject);
begin
  SequenceList := TStringList.Create();
  LastAnimationName := 'johnstandne';
  PlayForward := True;
  RunAnimationList(Scene1, ['johnstandfront', 'johnturnsw', 'johnstandsw', 'johnturnleft', 'johnstandleft', 'johnturnnw', 'johnstandnw', 'johnturnback', 'johnstandback',
 'johnturnne', 'johnstandne', 'johnturnright', 'johnstandright', 'johnturnse', 'johnstandse', 'johnturnfront', 'johnstandfront']);
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);
var I: integer;
begin
  if PlayForward then
  begin
    For I := 0 to High(AnimNames) do
    begin
      SequenceList.Add(AnimNames[I]);
    end;
  end else
  begin
    For I := High(AnimNames) downto 0 do
    begin
    SequenceList.Add(AnimNames[I]);
  end;
end;

  AnimList := SequenceList;
  NextAnim := 0;
  RunNextAnimation(Scene, nil);
end;


procedure TStateMain.RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
var
  NextAnimationName: String;
  Params: TPlayAnimationParameters;
begin
 {if NextAnim = AnimList.Count then
  begin
    FreeAndNil(SequenceList);
    Exit;
  end;}
 // if AnimList[NextAnim] = LastAnimationName then Exit;
  Label1.Caption:= 'current animation number: ' + IntToStr(NextAnim);
  Label2.Caption:= 'First to start animation in next call: ' + IntToStr(FirstAnim);

  if LastAnim then
  begin
    FirstAnim := NextAnim; // remember the index value of LastAnimationName so next call to RunAnimationList will start from this animation
    FirstAnimationName := LastAnimationName; // next call to RunNextAnimation should start with this animation
    FreeAndNil(SequenceList);
    LastAnim := False;
    Exit;
  end;

  NextAnimationName := AnimList[NextAnim];
  if NextAnimationName = LastAnimationName then LastAnim := True else

  Inc(NextAnim);

  Params := TPlayAnimationParameters.Create;
  try
    Params.Name := NextAnimationName;
    Params.Forward:= PlayForward;
    Params.StopNotification := @RunNextAnimation;
    Scene.PlayAnimation(Params);

    finally FreeAndNil(Params) end;
    end;

end.

animationsequence4.zip (48.1 MB)

You need to decide what LastAnim should do and when it should be set.

I would advise to not do this by modifying RunNextAnimation and RunAnimationList – you made them quite convoluted. It could probably be fixed, but TBH it looks complicated and would result in complication of a simple and working code.

Instead I would advise: if you want your animations to stop at some animation, just don’t pass more animations to RunAnimationList. I.e. instead of

  LastAnimationName := 'johnstandne';
  RunAnimationList(Scene1, ['johnstandfront', 'johnturnsw', 'johnstandsw', 'johnturnleft', 'johnstandleft', 'johnturnnw', 'johnstandnw', 'johnturnback', 'johnstandback',
 'johnturnne', 'johnstandne', 'johnturnright', 'johnstandright', 'johnturnse', 'johnstandse', 'johnturnfront', 'johnstandfront']);

→ just do

  RunAnimationList(Scene1, ['johnstandfront', 'johnturnsw', 'johnstandsw', 'johnturnleft', 'johnstandleft', 'johnturnnw', 'johnstandnw', 'johnturnback', 'johnstandback',
 'johnturnne', 'johnstandne']);

  // and put remaining animations list in another list, to be used by button click later:
  AnimationsScheduledForButton.Clear;
  AnimationsScheduledForButton.AddRange(['johnturnright', 'johnstandright', 'johnturnse', 'johnstandse', 'johnturnfront', 'johnstandfront']);

Use TCastleStringList to have AddRange available. Then your button click can just use AnimationsScheduledForButton (I think it has ToArray method to get list as array, if not you can do it on your side).

Thanks. But then I thought it would be easier to load all animations once and then play a range by setting the first animation and last animation, because else I would have to make Runanimation lists of every possible ‘turnaround’ sequence.
I have now made this: it loads all animations and they get a number. Then it searches for the first (which is the current) and last animation and plays all animations between until it reaches the last set animation. This works okay, except it makes the character sprite turn the longest “route.”
For instance ‘standfront’ is the first animation in the list: 0
So if the sprite has to turn from 'standse (facing southeast) to standfront, it should play forward (clockwise).
But it does not because 'standse’ has number 14 in the list, so it plays backward (counterclockwise).

So if the LastAnimation is close to FirstAnimation it should take a ‘short turn’ instead of turning the sprite all around.
So I guess there should be something edited in the SetAnimationRange procedure to achieve this.
Any idea?
:slight_smile:



type
  TStateMain = class(TUIState)
  private
    Viewport: TCastleViewport;
    AnimList: TStrings;
    FirstAnim, NextAnim, LastAnim: Integer;
    StopAnim: Boolean;
    FirstName: string;
    FirstAnimationName, LastAnimationName: String;
    PlayForward: Boolean;

    procedure SetAnimationRange;
    procedure StandSW(Sender: TObject);
    procedure StandLeft(Sender: TObject);
    procedure StandNW(Sender: TObject);
    procedure StandBack(Sender: TObject);
    procedure StandNE(Sender: TObject);
    procedure StandRight(Sender: TObject);
    procedure StandSE(Sender: TObject);
    procedure StandFront(Sender: TObject);

    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;
  Button1, Button2, Button3, Button4, Button5, Button6, Button7, Button8: TCastleButton;
  Label1, Label2: TCastleLabel;
  Scene1: TCastleScene;


implementation

uses SysUtils;

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

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


procedure TStateMain.SetAnimationRange;
var A, B: Integer;
List: array[0..20] of string;
begin
  AnimList := Scene1.AnimationsList;
  For A :=  0 to AnimList.Count-1 do
  begin
    List[A] := AnimList[A];
    if List[A] = FirstAnimationName then FirstAnim := A;
  end;

   For B :=  0 to AnimList.Count-1 do
  begin
    List[B] := AnimList[B];
    if List[B] = LastAnimationName then LastAnim := B;
  end;

 if FirstAnim > LastAnim then PlayForward := False;
 if FirstAnim < LastAnim then PlayForward := True;

 NextAnim := FirstAnim;

 end;


procedure TStateMain.Start;
begin

  inherited;

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

  Button1 := DesignedComponent('Button1') as TCastleButton;
  Button2 := DesignedComponent('Button2') as TCastleButton;
  Button3 := DesignedComponent('Button3') as TCastleButton;
  Button4 := DesignedComponent('Button4') as TCastleButton;
  Button5 := DesignedComponent('Button5') as TCastleButton;
  Button6 := DesignedComponent('Button6') as TCastleButton;
  Button7 := DesignedComponent('Button7') as TCastleButton;
  Button8 := DesignedComponent('Button8') as TCastleButton;

  Button1.Caption := 'standsw';
  Button2.Caption := 'standleft';
  Button3.Caption := 'standnw';
  Button4.Caption := 'standback';
  Button5.Caption := 'standne';
  Button6.Caption := 'standright';
  Button7.Caption := 'standse';
  Button8.Caption := 'standfront';

  Button1.OnClick:= @StandSW;
  Button2.OnClick:= @StandLeft;
  Button3.OnClick:= @StandNW;
  Button4.OnClick:= @StandBack;
  Button5.OnClick:= @StandNE;
  Button6.OnClick:= @StandRight;
  Button7.OnClick:= @StandSE;
  Button8.OnClick:= @StandFront;

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

  Scene1.Translation:= Vector3(800,300,0);

FirstName := 'john';

 Scene1.URL:= 'castle-data:/' + FirstName + '_idle.starling-xml';

FirstAnimationName := FirstName + 'standfront';
FirstAnim := 0;
SetAnimationRange;

end;

procedure TStateMain.StandSW(Sender: TObject);
begin
  LastAnimationName := FirstName + Button1.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandLeft(Sender: TObject);
begin
  LastAnimationName := FirstName + Button2.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandNW(Sender: TObject);
begin
  LastAnimationName := FirstName + Button3.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandBack(Sender: TObject);
begin
  LastAnimationName := FirstName + Button4.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandNE(Sender: TObject);
begin
  LastAnimationName := FirstName + Button5.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandRight(Sender: TObject);
begin
  LastAnimationName := FirstName + Button6.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandSE(Sender: TObject);
begin
  LastAnimationName := FirstName + Button7.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.StandFront(Sender: TObject);
begin
  LastAnimationName := FirstName + Button8.Caption;
  SetAnimationRange;
  RunAllAnimations(Scene1);
end;

procedure TStateMain.RunNextAnimation(const Scene: TCastleSceneCore; const Animation: TTimeSensorNode);
var
  NextAnimationName: String;
  Params: TPlayAnimationParameters;
begin
   if StopAnim then
  begin
    FirstAnim := NextAnim; // remember the index value of LastAnimationName so next call to RunAnimationList will start from this animation
    FirstAnimationName := LastAnimationName; // next call to RunNextAnimation should start with this animation
    StopAnim := False; // reset
    Exit;
  end;

  NextAnimationName := AnimList[NextAnim];
  if NextAnimationName = LastAnimationName then StopAnim := True else

  begin
    if PlayForward then Inc(NextAnim);
    if PlayForward = False then
    begin
      if NextAnim = 0 then NextAnim := AnimList.Count;
      Dec(NextAnim);
    end;
  end;

  Params := TPlayAnimationParameters.Create;
  try
    Params.Name := NextAnimationName;
    Params.Forward:= PlayForward;
    Params.StopNotification := @RunNextAnimation;
    Scene.PlayAnimation(Params);

    finally FreeAndNil(Params) end;
    end;

end.

It seems you want to calculate whether to go backward or forward through the animation list. There are various ways to go backward, but I would suggest the simplest route for you: just reverse the animation list when copying it at the beginning of RunAnimationList.

To decide whether to go forward or backward, you have to compare 2 distances: one is going forward through the animation list (“index of last animation - index of first animation”, and if it’s negative add the “count of animations”), the other distance is going backward (which should be equal “count of animations - distance of going forward”). Choose the option with the shortest distance.

I do not follow your code, I admit I don’t know what you want to do by SetAnimationRange. Looks like you want to hack RunAllAnimations into doing something contrary to its name (which would be “run all animations in the scene”). I would rather advise to go back to the working RunAnimationList that you had. Build new concepts on top of the existing concepts that you have working reliably.

Yes! This is what I was looking for but I could not get it working because I could not figure out how to calculate the backward playing distance.
Thanks!

Now I have this and it works perfectly :slight_smile:

procedure TStateMain.SetAnimationRange;
var A, B, AnimCountForward, AnimCountBackward: Integer;
List: array[0..20] of string;
begin
  AnimList := Scene1.AnimationsList;
  For A := 0 to AnimList.Count-1 do
  begin
    List[A] := AnimList[A];
    if List[A] = FirstAnimationName then FirstAnim := A;  // find current animationame and remember its index
  end;

  For B := 0 to AnimList.Count-1 do
  begin
    List[B] := AnimList[B];
    if List[B] = LastAnimationName then LastAnim := B;  // find destination animation and remember its index
  end;

  AnimCountForward := LastAnim - FirstAnim;  // count the forward number of animations between them
  If AnimCountForward < 0 then AnimCountForward := (AnimCountForward + AnimList.Count);

  AnimCountBackward := AnimList.Count - AnimCountForward; // count the backward number of animations between them
  if AnimCountBackward < AnimCountForward then PlayForward := False else PlayForward := True;

Yes, I should have named it like RunAnimationRange.

Ok, maybe I will try this for the above, but since it works at last now I dont’ dare to change again :slight_smile:
I think I will use RunAnimationList for playing certain animations in the sprite sheet and ‘RunAnimationRange’ only for turning the sprite.