Sprites
Unit: Sprite
Frame-rate independent sprite animation system.
Types
type
TSprite = record
Image: PImage;
Frames: array[0..63] of TRectangle;
FrameCount: Byte;
Width, Height: Word;
Duration: Real; { Total duration in seconds }
PlayType: Byte; { Forward/PingPong/Once }
end;
PSprite = ^TSprite;
TSpriteInstance = record
Sprite: PSprite;
X, Y: Integer;
FlipX, FlipY: Boolean;
Hidden: Boolean;
CurrentTime: Real; { Current time in seconds, -1.0 = stopped }
PlayBackward: Boolean;
end;
PSpriteInstance = ^TSpriteInstance;
Constants
const
SpritePlayType_Forward = 0; { Loop continuously: 0→1→2→3→0→... }
SpritePlayType_PingPong = 1; { Bounce: 0→1→2→3→2→1→0→... }
SpritePlayType_Once = 2; { Play once: 0→1→2→3 [STOP] }
MaxSpriteFrames = 64;
Functions
procedure UpdateSprite(var SpriteInstance: TSpriteInstance; DeltaTime: Real);
procedure DrawSprite(var SpriteInstance: TSpriteInstance; FrameBuffer: PFrameBuffer);
function SpriteGetCurrentFrame(var SpriteInstance: TSpriteInstance): Byte;
function CheckSpriteCollision(SpriteA, SpriteB: PSpriteInstance): Boolean;
UpdateSprite
Updates the sprite animation state based on elapsed time. Call once per frame with delta time.
DrawSprite
Renders the current frame to the framebuffer with transparency (color 0).
SpriteGetCurrentFrame
Returns the current frame index (0..FrameCount-1) based on CurrentTime and PlayType. Useful for manual frame access or debugging.
CheckSpriteCollision
Performs pixel-perfect collision detection between two sprite instances. Returns True if any non-transparent pixels overlap. Optimized with bounding box check and early exit. Accounts for FlipX/FlipY transformations.
Example
uses VGA, Sprite, PCX, RTCTimer;
var
SpriteSheet: TImage;
PlayerRun: TSprite;
Player: TSpriteInstance;
BackBuffer: PFrameBuffer;
LastTime, CurrentTime, DeltaTime: Real;
i: Integer;
begin
InitVGA;
InitRTC(1024);
BackBuffer := CreateFrameBuffer;
{ Load sprite sheet }
LoadPCX('PLAYER.PCX', SpriteSheet);
{ Setup sprite (8 frames, 32×32 each) }
PlayerRun.Image := @SpriteSheet;
PlayerRun.FrameCount := 8;
PlayerRun.Duration := 0.8; { 0.8 seconds total }
PlayerRun.PlayType := SpritePlayType_Forward;
{ Define frames (horizontal strip) }
for i := 0 to 7 do
begin
PlayerRun.Frames[i].X := i * 32;
PlayerRun.Frames[i].Y := 0;
PlayerRun.Frames[i].Width := 32;
PlayerRun.Frames[i].Height := 32;
end;
{ Create instance }
Player.Sprite := @PlayerRun;
Player.X := 144;
Player.Y := 84;
Player.CurrentTime := 0.0;
Player.FlipX := False;
Player.Hidden := False;
{ Animation loop }
LastTime := GetTimeSeconds;
while Running do
begin
{ Calculate delta time }
CurrentTime := GetTimeSeconds;
DeltaTime := CurrentTime - LastTime;
LastTime := CurrentTime;
{ Update and render }
UpdateSprite(Player, DeltaTime);
ClearFrameBuffer(BackBuffer);
DrawSprite(Player, BackBuffer);
RenderFrameBuffer(BackBuffer);
end;
FreeFrameBuffer(BackBuffer);
FreeImage(SpriteSheet);
DoneRTC;
DoneVGA;
end.
Multiple Animations
var
PlayerIdle, PlayerRun: TSprite;
Player: TSpriteInstance;
begin
{ Setup idle (4 frames, ping-pong) }
PlayerIdle.Image := @SpriteSheet;
PlayerIdle.FrameCount := 4;
PlayerIdle.Duration := 1.0;
PlayerIdle.PlayType := SpritePlayType_PingPong;
{ Define frames row 0... }
{ Setup run (8 frames, loop) }
PlayerRun.Image := @SpriteSheet;
PlayerRun.FrameCount := 8;
PlayerRun.Duration := 0.6;
PlayerRun.PlayType := SpritePlayType_Forward;
{ Define frames row 1... }
{ Switch animations }
if IsKeyDown(Key_D) then
begin
Player.Sprite := @PlayerRun;
Player.CurrentTime := 0.0; { Reset animation }
Player.FlipX := False;
end
else
begin
Player.Sprite := @PlayerIdle;
Player.CurrentTime := 0.0;
end;
end;
Sprite Sheet Layouts
Horizontal strip (common):
{ 5 frames, 32×32 each }
for i := 0 to 4 do
begin
Sprite.Frames[i].X := i * 32;
Sprite.Frames[i].Y := 0;
Sprite.Frames[i].Width := 32;
Sprite.Frames[i].Height := 32;
end;
Grid layout (4×2):
{ 8 frames }
for i := 0 to 7 do
begin
Sprite.Frames[i].X := (i mod 4) * 32;
Sprite.Frames[i].Y := (i div 4) * 32;
Sprite.Frames[i].Width := 32;
Sprite.Frames[i].Height := 32;
end;
Critical Notes
MUST use delta time - Never use fixed values like 0.016
Duration in seconds - Not frames or milliseconds
Reset animation - Set
CurrentTime := 0.0when switching spritesShared sprite data - Multiple instances can share one TSprite
CurrentTime < 0.0 - Means stopped (PlayType_Once finished)
Frame Duration
Duration = 0.8 seconds
FrameCount = 8 frames
Each frame displays for: 0.8 / 8 = 0.1 seconds
Stagger Animations
{ All enemies sync }
for i := 0 to 9 do
Enemies[i].CurrentTime := 0.0;
{ Stagger for organic look }
for i := 0 to 9 do
Enemies[i].CurrentTime := Random(1000) / 1000.0 * Sprite.Duration;
Collision Detection
var
Player, Enemy: TSpriteInstance;
Hit: Boolean;
begin
{ Update both sprites }
UpdateSprite(Player, DeltaTime);
UpdateSprite(Enemy, DeltaTime);
{ Check pixel-perfect collision }
Hit := CheckSpriteCollision(@Player, @Enemy);
if Hit then
begin
{ Handle collision }
WriteLn('COLLISION DETECTED!');
end;
{ Alternative: Check current frame manually }
if SpriteGetCurrentFrame(Player) = 3 then
WriteLn('Player is on attack frame');
end;
Collision features:
Pixel-perfect (ignores transparent pixels, color 0)
Bounding box optimization (early exit if boxes don’t overlap)
Flip-aware (handles FlipX/FlipY correctly)
Hidden-aware (returns False if either sprite is hidden)
Fast early exit (returns True on first pixel collision)
Notes
Frame-rate independent (works on any CPU speed)
Supports horizontal/vertical flipping
Color 0 = transparent when drawing
Use with RTCTIMER for accurate delta-time
See RESMAN.PAS for loading sprites from XML