Showing posts with label sprite. Show all posts
Showing posts with label sprite. Show all posts

Thursday, October 7, 2010

Observing sprites

TComponentEx implements IObserver and IObservable patterns, since TDummySprite inherites from the component, it carries the benefit. It is useful when you want to nofity to another observer a message, I use it in the Attach/Deattach methods this way:


procedure TSimpleSprite.SetAttachedSprite(const aValue: TSimpleSprite);
begin
  if aValue = self then
    raise Exception.Create('Self cannot be attached');

  FAttachedSprite := aValue;
  if FAttachedSprite <> nil then
  begin
    if aValue.IsDead then
      Exit;  //Sprite it's gonna be destroid in the next frame, therefore self cannot be longer notified by any other sprite

    FAttachX := X - FAttachedSprite.X;
    FAttachY := Y - FAttachedSprite.Y;
    FAttachedSprite.AddObserver(self); //In order to avoid following a dead sprite, a notification is added to the attached sprite
  end else
  begin
    FAttachX := 0;
    FAttachY := 0;
  end;
end;

procedure TSimpleSprite.DeattachSprite;
begin
  if FAttachedSprite <> nil then
  begin
    FAttachedSprite.DeleteObserver(self);
    FAttachedSprite := nil;
    FAttachX := 0;
    FAttachY := 0;
  end;
end;

procedure TSimpleSprite.DeleteObserver(aObserver: IObserver);
begin
  inherited;

  if aObserver = FAttachedSprite as IObserver then
    FAttachedSprite := nil;
end;
This way we always make sure that we don't follow an already freed sprite. The 
interfaces look like this:
IObserver = interface
['{3E91264F-BBC0-44DF-8272-BD8EA9B5846C}']
    procedure Update(aMessage: TMessage);
  end;

  IObservable = interface
    ['{A7C4D942-011B-4141-97A7-5D36C443355F}']
    procedure AddObserver(aObserver: IObserver);
    procedure DeleteObserver(aObserver: IObserver);
    procedure ClearObservers;
    procedure Notify(aMessage: TMessage);
  end;
When a TComponentEx is destroid this code is launch:
destructor TComponentEx.Destroy;
var
  m: TMessage;

begin
  m := TMessage.MessageDestroy(self);
  try
    FObservable.Notify(m);
  finally
    m.Free;
  end;
  FObservable.Free;

  inherited;
end;
The Notify() method:
procedure TObservable.Notify(aMessage: TMessage);
var
  i: integer;

begin
  //Notify a message to interested observers
  for i := FNotify.Count - 1 downto 0 do
    IObserver(FNotify[i]).Update(aMessage);
end;
the Update() method:
procedure TObservable.Update(aMessage: TMessage);
var
  I: IObserver;

begin
  if aMessage = nil then
    Exit;

  //An observer is informing me that is going to be freed
  if aMessage.Id = MSG_DESTROY then
  begin
    if aMessage.Sender.GetInterface(IObserver, I) then
      DeleteObserver(I);
  end;
end;
(Update is a virtual method so its funcionality can be expanded easily)
Finally a TMessage:
const
  MSG_DESTROY = 1;
  MSG_CHANGED = 2;
  MSG_RESTORE = 3;

type
  TMessage = class
  private
    FId: integer;
    FText: string;
    FSender: TObject;
  public
    constructor Create(aSender: TObject; aId: integer; const aText: string); overload;
    constructor Create(aSender: TObject; aId: integer); overload;
    constructor Create(aSender: TObject; const aText: string); overload;
    class function MessageDestroy(aSender: TObject): TMessage;

    property Sender: TObject read FSender;
    property Id: integer read FId;
    property Text: string read FText;
  end;
The Salamander project relies on this mechanism to find out that THero class has been freed (the "game" continue running but all entities ignore the hero ship). Somewhere in the code you can find:
procedure TDragon.SetGame(const aValue: TGame);
begin
  inherited SetGame(avalue);
  if GetGame.Hero <> nil then
GetGame.Hero.AddObserver(self);
end;
and
procedure TMyGame.DeleteObserver(aObserver: IObserver);
begin
  inherited;

  //Hero
  if aObserver = FHero as IObserver then
  begin
    FHero := nil;
    Exit;
  end;
end;

Ciao

Sunday, October 3, 2010

BB, part 2, creating a sprite

uses
  BB.Screen.D3D, BB.Screen.Types, BB.Input.Keys, BB.Colors, BB.Screen,
  BB.Screen.Surfaces, BB.Screen.Sprites, BB.Screen.Engines;


procedure TForm1.FormActivate(Sender: TObject);
var
  DD: TGraphicDriver;
  e: TSpriteEngine;
  demon, hero: TMaskedSprite;
  x, y: integer;

begin
  DD := TD3DDriver.Create;
  try
    DD.WindowHandle := WindowHandle;
    DD.Width := Width;
    DD.Height := Height;
    DD.BPP := 32;
    DD.ScreenFlags := [sfClear];
    DD.MaxFPS := 100;
    DD.Initialize;

    e := TSpriteEngine.Create;
    e.FileNames.Add('demon.bmp');
    e.FileNames.Add('fire.bmp');
    e.FileNames.Add('hero.bmp');
    e.LoadFile;

    demon := TMaskedSprite.Create;
    demon.Engine := e;
    demon.Masked := True;
    demon.AnimationName := 'demon';
    demon.GoCenter;
    demon.CollisionShape := csPrecise;

    hero := TMaskedSprite.Create;
    hero.Engine := e;
    hero.Masked := True;
    hero.AnimationName := 'hero';
    hero.GoCenterY;
    hero.CollisionShape := csPrecise;

    repeat
      DD.BackgroundColor := 0;

      if Inkey(VK_LEFT) then
        hero.x := hero.x - 1
      else if Inkey(VK_RIGHT) then
        hero.x := hero.x + 1;

      if Inkey(VK_UP) then
        hero.y := hero.y - 1
      else if Inkey(VK_DOWN) then
        hero.y := hero.y + 1;

      hero.Masked := True;
      demon.Masked := True;
      if hero.Collision(demon, x, y) then
        DD.BackgroundColor := TRGB.Blue.ToInt(32);

      hero.Masked := False;
      demon.Masked := False;
      if hero.Collision(demon, x, y) then
        DD.BackgroundColor := TRGB.Red.ToInt(32);

      DD.BeginRender;
      e.Render;
      DD.EndRender;

      Application.ProcessMessages;
    until Inkey(VK_ESCAPE);

  finally
    DD.Free;
  end;

  Close;
end;

end.



A TSpriteEngine is a container of sprites that also offers some loading methods, one of them is for every bitmap set create a .ini that configures the behaviour:

[cfg]
Method=Fix
Width=240
Height=144

[demon]
Frames=2,1,0
Type=PingPong
Cadency=10

[demon_mask]
Frames=5,4,3
Type=PingPong
Cadency=10




This way the engine knows what to do once you assign "demon" no the AnimationName property of the TSpriteAnimation class. The possible values for the [cfg] method are: 
  • Manual  (you set the values manually via Index option)
  • Circular (once the last frame is reached, next frame is the first one)
  • PingPong (once the last frame is reached, it goes back to the first one and again...)
  • Lineal (once the last frame is reached, nothing happens...)
  • LinealFree (once the last frame is reached, the sprite is freed)