Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions examples/spriteAnim.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// This example may be large, so use regions for navigating faster. Important
// content is marked with 👁️

kaplay({ scale: 4, font: "happy" });
kaplay({ scale: 4, font: "happy", background: "#aee2ff" });

const SPEED = 120;
const JUMP_FORCE = 240;
Expand Down Expand Up @@ -51,7 +51,7 @@ loadSprite("dino", "/sprites/dungeon-dino.png", {

// Add our player character 👁️
const player = add([
sprite("dino"),
sprite("dino", { anim: "jump" }),
pos(center()),
anchor("center"),
area(),
Expand All @@ -78,7 +78,7 @@ This time we're defining a function for executing animations conditionally.
const playerPlayRun = () => {
// obj.play() will reset to the first frame of the animation
// so we want to make sure it only runs when the current animation is not "run"
if (player.isGrounded() && player.getCurAnim().name !== "run") {
if (player.isGrounded() && player.getCurAnim()?.name !== "run") {
player.play("run");
}
};
Expand Down Expand Up @@ -115,6 +115,10 @@ onKeyPress(["space", "up"], () => {
}
});

onKeyPress("x", () => {
player.stop();
});

// Switch to "idle" or "run" animation when player hits ground
player.onGround(() => {
if (!isKeyDown("left") && !isKeyDown("right")) {
Expand All @@ -127,18 +131,40 @@ player.onGround(() => {

// #endregion

// You can run functions when a specific animation ends 👁️
// You can run functions on specific animation moments 👁️
let loopCount = 0;

player.onAnimStart((anim) => {
loopCount = 0;
});

player.onAnimEnd((anim) => {
if (anim === "idle") {
debug.log("hi!");
// Will never run as idle is set to loop: true
debug.log("idle animation ended");
}
else if (anim === "jump") {
debug.log("jump animation ended");
}
});

player.onAnimStop((anim) => {
if (anim === "idle") {
debug.log("idle animation stopped");
}
});

player.onAnimLoop((anim) => {
loopCount++;
});

// #region UI
const getInfo = () =>
`
Anim: ${player.getCurAnim()?.name}
Frame: ${player.frame}
Loops: ${loopCount}
Press (x) to stop anim
`.trim();

// Add some text to show the current animation
Expand Down
154 changes: 154 additions & 0 deletions examples/spriteAnim3001.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* @file Sprite Animations
* @description How to load and animate sprites
* @difficulty 0
* @tags basics, animation
* @minver 3001.0
* @category basics
*/

// Animate Sprites with platformer movement [👁️]

// This example may be large, so use regions for navigating faster. Important
// content is marked with 👁️

kaplay({ scale: 4, font: "happy" });

const SPEED = 120;
const JUMP_FORCE = 240;
setGravity(640);

// #region Loading Assets 👁️
loadBitmapFont("happy", "/fonts/happy_28x36.png", 28, 36);

// Loading a multi-frame sprite 👁️
loadSprite("dino", "/sprites/dungeon-dino.png", {
// The image contains 9 frames layered out horizontally, slice it into individual frames
sliceX: 9,
// Define animations
anims: {
"idle": {
// Starts from frame 0, ends at frame 3
from: 0,
to: 3,
// Frame per second
speed: 5,
loop: true,
},
"run": {
from: 4,
to: 7,
speed: 10,
loop: true,
},
// This animation only has 1 frame
"jump": 8,
},
});
// #endregion

// #region Game Objects

// Add our player character 👁️
const player = add([
sprite("dino", { anim: "jump" }),
pos(center()),
anchor("center"),
area(),
body(),
]);

// Add a platform
add([
rect(width(), 24),
area(),
outline(1),
pos(0, height() - 24),
body({ isStatic: true }),
]);
// #endregion

/* 👁️
We can animate sprites using obj.play("name") method.

This time we're defining a function for executing animations conditionally.
*/

// #region Player animations 👁️
const playerPlayRun = () => {
// obj.play() will reset to the first frame of the animation
// so we want to make sure it only runs when the current animation is not "run"
if (player.isGrounded() && player.getCurAnim().name !== "run") {
player.play("run");
}
};

const playerPlayIdle = () => {
// Only reset to "idle" if player is not holding any of these keys
if (player.isGrounded() && !isKeyDown("left") && !isKeyDown("right")) {
player.play("idle");
}
};
// #endregion

// #region Player move/anim 👁️
onKeyDown("left", () => {
player.move(-SPEED, 0);
player.flipX = true;
playerPlayRun();
});

onKeyDown("right", () => {
player.move(SPEED, 0);
player.flipX = false;
playerPlayRun();
});

onKeyRelease(["left", "right"], () => {
playerPlayIdle();
});

onKeyPress(["space", "up"], () => {
if (player.isGrounded()) {
player.jump(JUMP_FORCE);
player.play("jump");
}
});

// Switch to "idle" or "run" animation when player hits ground
player.onGround(() => {
if (!isKeyDown("left") && !isKeyDown("right")) {
player.play("idle");
}
else {
player.play("run");
}
});

// #endregion

// You can run functions when a specific animation ends 👁️
player.onAnimEnd((anim) => {
if (anim === "idle") {
debug.log("hi!");
}
});

// #region UI
const getInfo = () =>
`
Anim: ${player.getCurAnim()?.name}
Frame: ${player.frame}
`.trim();

// Add some text to show the current animation
const label = add([
text(getInfo(), { size: 12 }),
color(0, 0, 0),
pos(4),
]);

label.onUpdate(() => {
label.text = getInfo();
});
// #endregion
64 changes: 57 additions & 7 deletions src/ecs/components/draw/sprite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export interface SpriteCurAnim {
*/
frameIndex: number;
pingpong: boolean;
onEnd: () => void;
onEnd?: () => void;
onLoop?: () => void;
onStop?: () => void;
}

/**
Expand Down Expand Up @@ -82,7 +84,7 @@ export interface SpriteComp extends Comp {
/**
* Play a piece of anim.
*/
play(anim: string, options?: SpriteAnimPlayOpt): void;
play(anim: string, opt?: SpriteAnimPlayOpt): void;
/**
* Stop current anim.
*/
Expand Down Expand Up @@ -129,8 +131,34 @@ export interface SpriteComp extends Comp {
onAnimStart(action: (anim: string) => void): KEventController;
/**
* Register an event that runs when an animation is ended.
*
* End is triggered by:
*
* - Animation reaching the last frame.
*/
onAnimEnd(action: (anim: string) => void): KEventController;
/**
* Register an event that runs when an animation is looped.
*
* Loop is triggered by:
*
* - Animation reaching the last frame for looping (last one or first/last in pingpong)
*
* @since v4000.0
*/
onAnimLoop(action: (anim: string) => void): KEventController;
/**
* Register an event that runs when an animation is stopped.
*
* Stop is triggered by:
*
* - obj.stop();
* - obj.play();
* - Animation ending (like onEnd)
*
* @since v4000.0
*/
onAnimStop(action: (anim: string) => void): KEventController;
/**
* @since v3000.0
*/
Expand Down Expand Up @@ -476,10 +504,13 @@ export function sprite(
}
else if (curAnim.loop) {
curAnim.frameIndex = 0;
curAnim.onLoop?.();
this.trigger("animLoop", curAnim.name);
}
else {
this.frame = frames.at(-1)!;
curAnim.onEnd();
this.trigger("animEnd", curAnim.name);
curAnim.onEnd?.();
this.stop();
return;
}
Expand All @@ -491,10 +522,13 @@ export function sprite(
}
else if (curAnim.loop) {
curAnim.frameIndex = frames.length - 1;
curAnim.onLoop?.();
this.trigger("animLoop", curAnim.name);
}
else {
this.frame = frames[0];
curAnim.onEnd();
this.trigger("animEnd", curAnim.name);
curAnim.onEnd?.();
this.stop();
return;
}
Expand Down Expand Up @@ -533,7 +567,6 @@ export function sprite(
pingpong: false,
speed: 0,
frameIndex: 0,
onEnd: () => {},
}
: {
name: name,
Expand All @@ -542,7 +575,9 @@ export function sprite(
pingpong: opt.pingpong ?? anim.pingpong ?? false,
speed: opt.speed ?? anim.speed ?? 10,
frameIndex: 0,
onEnd: opt.onEnd ?? (() => {}),
onEnd: opt.onEnd,
onLoop: opt.onLoop,
onStop: opt.onStop,
};

curAnimDir = typeof anim === "number" ? null : 1;
Expand All @@ -555,9 +590,10 @@ export function sprite(
if (!curAnim) {
return;
}
curAnim.onStop?.();
const prevAnim = curAnim.name;
curAnim = null;
this.trigger("animEnd", prevAnim);
this.trigger("animStop", prevAnim);
},

numFrames() {
Expand Down Expand Up @@ -594,6 +630,20 @@ export function sprite(
return this.on("animStart", action);
},

onAnimLoop(
this: GameObj<SpriteComp>,
action: (name: string) => void,
): KEventController {
return this.on("animLoop", action);
},

onAnimStop(
this: GameObj<SpriteComp>,
action: (name: string) => void,
) {
return this.on("animStop", action);
},

renderArea() {
if (!_shape) {
_shape = new Rect(vec2(0), _width, _height);
Expand Down
Loading