Animation and particles
Back in Images and sprites, a sprite changed its frame because the loop told it to. That’s fine for one light blinking, but a game is full of things that animate: a walk, a spinning coin, a flame. A behavior can step through the frames for me, so your loop never has to count them.
A sheet of frames
explosion.png is in every game’s folder. It’s a sheet of 32 × 32 frames, so
it loads with !! spritesheet((32, 32)), and framecount says how many
frames that made.
SCREEN 320, 240
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
PRINT "the explosion has", framecount("boom"), "frames"
the explosion has 16 frames
Playing them
ANIMATE goes in a behavior. It takes the image, the frames to play in
order, and after !!, how many frames a second to show them at. Frames
0 ... 15 is all sixteen, and it starts again from the first when it gets to
the end.
SCREEN 320, 240
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
BEHAVIOR "burning"
ANIMATE "boom", 0 ... 15 !! framerate(12)
END BEHAVIOR
ENABLE SPRITE 0
SET SPRITE 0 !! image("boom") frame(0) x(160) y(120)
!! origin((0.5, 0.5)) scale((3, 3))
!! behavior("burning")
SHOW SPRITE 0
PAUSE
Try framerate(30) for a faster fire, or [4, 5, 6, 7] to play only the
middle of it. The frames don’t have to be in order, either: [0, 1, 2, 1]
goes there and back.
Standing and running
A hero has more than one animation: one for standing about and one for
running, each a sheet of its own. hero_idle.png and hero_run.png are
sheets of 40 × 40 frames. Give each its own behavior, and when the hero starts
or stops running, take one behavior off and put the other on. ANIMATE
changes the sprite’s picture to its own sheet as it starts, so the sprite
always shows the one that’s playing.
SCREEN 320, 240
LOADIMG "hero_idle.png", "idle" !! spritesheet((40, 40))
LOADIMG "hero_run.png", "run" !! spritesheet((40, 40))
BEHAVIOR "standing"
ANIMATE "idle" !! framerate(6)
END BEHAVIOR
BEHAVIOR "running"
ANIMATE "run" !! framerate(12)
END BEHAVIOR
x = 120
ENABLE SPRITE 0
SET SPRITE 0 !! image("idle") frame(0) x(x) y(80) scale((2, 2))
!! behavior("standing")
SHOW SPRITE 0
running = FALSE
DO
IF COOLDOWN(1)
way = keydown("Right") - keydown("Left")
x = x + way * 2
IF x < 0 THEN x = 0
IF x > 240 THEN x = 240
SET SPRITE 0 !! x(x)
IF way < 0 THEN SET SPRITE 0 !! flip_x
IF way > 0 THEN SET SPRITE 0 !! flip_x(FALSE)
IF (way <> 0) <> running
running = way <> 0
IF running
SET SPRITE 0 !! remove_behavior("standing")
SET SPRITE 0 !! behavior("running")
ELSE
SET SPRITE 0 !! remove_behavior("running")
SET SPRITE 0 !! behavior("standing")
END IF
END IF
END IF
LOOP
Click the screen and hold an arrow key. With no frames listed, ANIMATE
plays every frame in the sheet. The loop only swaps the behaviors when
running starts or stops: giving the sprite "running" again every tick
would start the run over from its first frame, every tick.
scale((2, 2)) shows each pixel of the art as two by two, and that’s the
way to make pixel art bigger: by a whole number. Half sizes and odd sizes
smear the pixels.
Once, and gone
An explosion shouldn’t go round and round. !! noloop plays the frames once
and holds the last one. Then the sprite has to go, and that’s what a
particle is for.
EMITPARTICLE makes a sprite that looks after itself: where it starts, the
image, a tag, the behavior that drives it, and how many ticks it lives. When
its time’s up it’s gone, and its number goes back to me. You don’t
ENABLE, SHOW or DISABLE anything.
SCREEN 320, 240
RANDOMIZE
LOADIMG "explosion.png", "boom" !! spritesheet((32, 32))
LOADWAV "explosion.wav", "bang"
BEHAVIOR "explode"
ANIMATE "boom", 0 ... 15 !! framerate(30) noloop
END BEHAVIOR
DO
IF COOLDOWN(40)
EMITPARTICLE (20 + rnd * 250, 20 + rnd * 180), "boom", "boom", "explode", 32 !! scale((2, 2))
PLAYWAV "bang", 0, 0.3
END IF
LOOP
Sixteen frames at thirty a second take 32 ticks, so each explosion lives
exactly as long as its animation. The !! scale((2, 2)) after it is the same
scale a sprite takes: a particle takes most of what SET SPRITE does.
Sparks
A particle can be driven by any behavior, and one behavior can send each
particle its own way: give it settings with params, the way
behavior_params does for a sprite. Here’s a fountain of sparks, each one
thrown up at its own angle, pulled down, and fading as it goes.
SCREEN 320, 240
RANDOMIZE
MEMORYBUFFER "spark", 4, 4
LINE (0, 0) -> (3, 3), 14 !! box fill
END MEMORYBUFFER
BEHAVIOR "spark"
IMPULSE PARAM("vx", 0), PARAM("vy", -3)
ACCELERATION 0, 0.1
ALPHA 1, 0, 50
END BEHAVIOR
DO
IF COOLDOWN(2)
vx = rnd * 3 - 1.5
vy = -2 - rnd * 3
EMITPARTICLE (160, 200), "spark", "spark", "spark", 50 !! params({vx: vx, vy: vy})
END IF
IF COOLDOWN(30)
FRAME
CLS
LOCATE 1, 1
PRINT "sparks in the air:", spritecount("spark")
END FRAME
END IF
LOOP
A new spark every two ticks, each living fifty: there are never more than twenty-five at once, and I tidy up every one of them.
Crash-tan’s Tip:
EMITPARTICLEis also a function,emitparticle, that gives back the particle’s sprite number, in case you want to do something more to it.
Blink and shake
Two more things a behavior can do to how a sprite looks. BLINK 6 hides and
shows it every six ticks, the classic “I just got hit, I can’t be hurt for a
moment” flicker. SHAKE 2 jiggles it up to two pixels either way. Put them
in a behavior of their own, and add it and take it away again with
behavior and remove_behavior.
SCREEN 320, 240
LOADIMG "hero.png", "tan"
BEHAVIOR "hurt"
BLINK 6
SHAKE 2
END BEHAVIOR
ENABLE SPRITE 0
SET SPRITE 0 !! image("tan") x(160) y(120) origin((0.5, 0.5)) behavior("hurt")
SHOW SPRITE 0
hurt = 60
DO
IF COOLDOWN(1)
IF keydown("Space") AND hurt = 0
SET SPRITE 0 !! behavior("hurt")
hurt = 60
END IF
IF hurt > 0
hurt = hurt - 1
IF hurt = 0 THEN SET SPRITE 0 !! remove_behavior("hurt")
END IF
END IF
LOOP
The hero starts out hurt. Once they’ve settled, click the screen and press Space, and they flicker and tremble for another second.
That’s a lot of moving and looking that the loop doesn’t have to think about. Next, a way to tell a sprite to do one thing, then another, then another.