Encapsulating player behavior

I notice that in the platformer example as well as the templates, the player behavior is coded directly in the gameplay ui state (ie, the code handling input and setting velocity of the player, etc). I usually like to encapsulate all of that player behavior in it’s own class…and maybe even separate out the player controller as another separate class that can be applied to the player (to be able to switch control modes, for example, if a player morphs from a fish to a walking animal or something like that, i can swap out its player controller)

I was able to do this, but would love some feedback if my approach sounds reasonable or if there is a better way to handle this. Here is what I did:

Created a TCastleScene ScenePlayer in the editor and assigned a png file to the URL.

Created a new class TPlayer descended from TCastleBehavior with a TCastleScene field called ScenePlayer

In TPlayer I override ParentChanged and set a reference to the ScenePlayer field there.

This is when I realized in order to handle input in the player class as I’m used to doing, I’d need a reference to the Container object of the UIState. I wasn’t sure if there is a way to get that from this context, so I added an argument to my TPlayer constructor to allow the gamestate to pass the container to TPlayer. That way in TPlayer.update I can check the input state and move my player as necessary.

The only catch is that when I was expecting to create my TPlayer object in the start procedure of the UI State, the container is nil at that point. I made kind of an ugly workaround which leads me to believe I’m not doing things the right way. Basically, in the update of the UIState, I do this to create my player once the container exists:

if not assigned(Player) and assigned(Container) then begin
Player := TPlayer.Create(nil, Container);
ScenePlayer.AddBehavior(Player);
end;

This works fine, but is ugly to do that in the update procedure.

If there is a better approach or suggestion to separating out player behavior and perhaps player control behavior from the main UIState class, I’d love know. I’d like to be able to get my bearings and try out most of the usual stuff I like to do in a game to see how to approach it in CGE.

thanks!

Hi, nice presentation today on Delphicon!

So I guess my question here is just, is there a correct way to access key and mouse events from inside a custom class that I make (to encapsulate player behavior for example). As I explained, I sort of rigged a way to do that which works, but didn’t seem like a good way to go. Any suggestion would be appreciated. (and of course, if there is a reason I shouldn’t be doing that, please let me know)

I’m afraid I won’t be able to provide you with a good answer, but hopefully may direct you in a right direction until someone more experienced will give a more specific answer.

So, overall, yes. You can and should separate player handling logic from the TUiState. A good programming design is to separate everything as much as possible. In simple cases though it’s more convenient to have everything “dumped into one entity” so that you won’t have to think about architecture (e.g. the same nil problem you’ve mentioned). So in simple prototypes or even simple games the player’s behavior is often just "handling some stuff in TUiState.Update.

Basically, in the update of the UIState, I do this to create my player once the container exists:

This is a relatively valid way of handling things that have to be created runtime. However, not knowing your exact reasons for this, I’d suggest creating Player entity in Start or even Create as in:

TMyUiState = class(TCastleUiState)
strict private
  Player: TPlayer;
public
  constructor Create(AOwner: TComponent); override;
end;

implementation
constructor TMyUiState.Create(AOwner: TComponent);
begin
  inherited;
  Player := TPlayer.Create(Self);
end;

You don’t have to strictly pass Container to Player as inside TPlayer you may write something like:

function TPlayer.PlayerContainer: TUiContainer;
begin
  Result := (Owner as TUiState).Container;
end;

If the TUiState has started Container should not be nil. I believe if the Window is open the Container should already be non-nil and like forever, but I can’t tell 100%.

If you want to add Player behavior to some TCastleScene from a design in Editor it may be most reasonable to add it in Start then, when the design is loaded already. And also free it at Stop when it’s no longer used, which will also make sure your TPlayer will “reset” appropriately when the game is restarted.

E.g. something like this:

procedure TMyUiState.Start;
begin
  inherited;
  MyScene := DesignedComponent('MyScene') as TCastleScene;
  MyPlayer := TPlayer.Create(MyScene);
  MyScene.AddBehavior(MyPlayer);
end;

Great, thanks for the suggestions! I’ll try it out tonight and see what I can do.

1 Like

I can confirm that Container is nil in the Start procedure of the uistate. I remember now that is actually where I began from and was surprised it was nil at that point, so did the workaround in update, but I still don’t like that solution. I do think your idea of passing the uistate to the player class in the constructor is good as anything in the uistate will then be accessible. I was sort of expecting there to be a start procedure in TCastleBehavior which would make sense to setup the container in, but there isn’t one to override.

Is it possible that there is a better or more proper way to check for key/mouse events than using Container? I haven’t seen anything in the examples that suggest this so just wondering.

For me, running the code below results in “not assigned” showing in the log.

procedure TStatePlay.Start;
begin
  inherited;

  { Find components, by name, that we need to access from code }
  LabelFps := DesignedComponent('LabelFps') as TCastleLabel;
  MainViewport := DesignedComponent('MainViewport') as TCastleViewport;
  ScenePlayer := DesignedComponent('ScenePlayer') as TCastleScene;

  Player := TPlayer.Create(self);
  ScenePlayer.AddBehavior(Player);
  if not assigned(Container) then begin

       Writelnlog('not assigned');

  end;
  WriteLnLog('setting up player');

end;         

Note that each and every child of TCastleUserInterface has Container property which is defined when the component is added to TUiState. So you don’t necessarily have to parent it to TUiState directly, as I’ve done in the second example - just use as TCastleUserInterface (it would also work for TUiState as TUiState is a child of TCastleUserInterface.

Depends. For simple goals, using Container is usually the best way to go. For more complex scenarios overriding function Press and friends may be more powerful way to handle user input. You’ll still have to handle that in behavior’s parent though - behavior doesn’t receive user input directly. Something like:

function TMyState.Press(const Event: TInputPressRelease): Boolean;
begin
  Result := inherited;
  if Result then
    Exit;
  MyPlayerBehavior.Press(Event);
end;

And same for Release and maybe Motion. A similar way of handling input is done here: https://gitlab.com/EugeneLoza/kryftolike/-/blob/master/code/gamestatemain.pas#L199 though it does some pre-processing before sending the actual movement command to the Player.

However, when the movement is “constant” (as in action game) then indeed reading the state of Container may be a more reasonable way.

In such a situation you may try Castle Game Engine: CastleUIState: Class TUIStateWaitForRenderAndCall literally waits for first render (when Container is most certainly initialized) and then calls this method.

@funemaker I’m glad you liked the DelphiCon presentation. Sorry for the delay in answering – preparing that presentation (and related Delphi port) took a loong time and then I needed a rest :slight_smile:

As for player class: confirming what you and @eugeneloza said, indeed it is good in non-trivial games to have a player as a separate class. You can do it in a number of ways

  1. completely independent new TPlayer class (that is, descending from TObject)
  2. TPlayer descends from TCastleScene or TCastleTransform
  3. TPlayer descends from TCastleBehavior (and thus can be attached to any TCastleScene or TCastleTransform)

All options make sense. Depending on the situation, I myself would either go for AD 1 or AD 3.

( AD 3 is a bit better than AD 2, as TCastleBehavior can be attached / detached at runtime and this gives extra flexibility. So I would not go for AD 2. )

( Note: we have TPlayer class in CastlePlayer unit, this is part of Utilities for typical 3D games . But I would not advise using it in new games, it is too specific for particular game types, and it will be deprecated soon. It’s better to roll your own TPlayer class. )

As for accessing the Container: indeed it is not set early, during TUIState.Start execution. See Castle Game Engine: CastleUIState: Class TUIState

  1. MyState.Start is called.
  2. MyState is added to the StateContainer.Controls list, …

The Container is a property inherited from TCastleUserInteface and it is only set at step 3, so after State is executed. This is how all TCastleUserInteface descendants behave, so we don’t want to change it, to keep things consistent.

Solutions:

  1. Use TUIState.StateContainer, which is available always.

  2. Actually, even simpler: from any place in the application, you can just access Application.MainWindow.Container – and use this to read current state of keys being pressed.

This allows you to observe the current state of keys being pressed.

Note that one can also listen for key press/release by overriding TCastleTransform.Press / TCastleTransform.Release, if TCastleTransform.ListenPressRelease is defined. We could use this to define also TCastleBehavior.Press / TCastleBehavior.Release virtual methods. Let me know if this seems useful – it would be trivial, and consistent, to make.

1 Like

Oh, and note: for receiving mouse events, there is also

  • TCastleTransform.PointingDevicePress
  • TCastleTransform.PointingDeviceRelease
  • TCastleTransform.PointingDeviceMove

Currently they can be overridden at TCastleTransform. We could expose them to be overridden also at TCastleBehavior.

1 Like

Great, thanks for the info! Indeed this question came about because I’m used to making my own player class and handling input in it’s update function or even delegating it further to a separate controller class. My TPlayer descends from TCastleBehavior so I’m glad that seems to be a “Castle-like” way to do things. I think the piece I was missing was getting the Container from Application.MainWindow which sounds convenient. I’m on vacation for all of this week but will have to try it out next week and see what other questions may come up…basically I’m just trying to see how all my normal approaches to things will apply in Castle.

1 Like