pixijs H5游戏开发入门(五)精灵动画及精灵表动画

本系列文章

本文编写时 pixijs 版本: v7.3.2

PIXI 游戏动画制作

前面文章介绍了文本、几何图形、图片资源的绘制,到此我们已经可以完成游戏场景制作了,但是还无法让场景动起来,本文介绍如何让 PIXI 中的元素动起来。

说到动画,那必须要先了解网页程序中如何让一个元素动起来:

元素运动依赖逐帧绘制完成,不管是 CSS 动画还是 JS 动画,都需要一个调度程序完成绘制操作。

CSS 动画由浏览器完成,JS 动画由定时器完成。PIXI 中的动画调度程序由 Ticker 完成,当然也可以不使用自带的调度程序,自己根据 requestAnimationFrame 实现调度。

几何图形动画

效果:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
async function init() {
// 创建一个 pixi 实例
let app = new PIXI.Application({
antialias: true, // 消除锯齿
width: 300,
height: 84
});

// 插入到 body 中
document.body.appendChild(app.view);

// 矩形绘制
let rectangle = new PIXI.Graphics();
rectangle.lineStyle(4, 0xFF3300, 1);
rectangle.beginFill(0x66CCFF);
rectangle.drawRect(0, 0, 64, 64);
rectangle.endFill();
rectangle.x = 10;
rectangle.y = 10;
// 将矩形添加到舞台
app.stage.addChild(rectangle);

app.ticker.add(delta => {
// delta = (当前帧的时间 - 上一帧的时间) / (1000 / 60); 其中1000是指1000ms,即1秒, 60是1秒种执行60次。 (1000 / 60)就是每秒钟执行60次,帧与帧之前的平均时间间隔。
// 矩形向右移动 1 像素
// rectangle.x += 1;

// 移出边界之后重置为 0
rectangle.x = (rectangle.x + 1) % (app.renderer.width + rectangle.width);
});

}

init();

上述代码利用 app.ticker.add 添加循环调度程序到 ticker ,由 PIXI 帮我们完成动画逐帧绘制。

再利用 取余运算符 % 判断动画移除边界之后回到开始位置。

精灵动画

效果:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
async function init() {
// 创建一个 pixi 实例
let app = new PIXI.Application({
antialias: true, // 消除锯齿
width: 300,
height: 84
});

// 插入到 body 中
document.body.appendChild(app.view);

await PIXI.Assets.load(['icon.jpg']);

// 使用独立的 ticker 控制动画
const ticker2 = PIXI.Ticker.shared;
let sprite2 = new PIXI.Sprite(PIXI.Assets.get("icon.jpg").clone());
sprite2.width = 18;
sprite2.height = 18;
sprite2.x = 10;
sprite2.y = 10 + 32 + 10;
// 使用速度属性(vx和vy)来控制精灵的移动速度
sprite2.vx = 1;
sprite2.vy = 1;
app.stage.addChild(sprite2);
ticker2.add(delta => {
if (sprite2.width + sprite2.x + 10 > app.renderer.width) {
// 触底判断
sprite2.vx = -1;
} else if (sprite2.x < 10) {
// 触顶判断
sprite2.vx = 1;
}
if (sprite2.height + sprite2.y + 10 > app.renderer.height) {
// 触底判断
sprite2.vy = -1;
} else if (sprite2.y < 10) {
// 触顶判断
sprite2.vy = 1;
}
sprite2.x += sprite2.vx;
sprite2.y += sprite2.vy;
});
}

init();

上述代码利用了 PIXI.Ticker.shared 完成动画调度,并未使用 app.ticker.add ,可以理解为 app.ticker.add 为整个舞台的动画调度程序,PIXI.Ticker.shared 为独立的动画调度程序。可利用 PIXI.Ticker.shared 完成独立的动画暂停与播放。

动画的暂停与播放

效果:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
async function init() {
// 创建一个 pixi 实例
let app = new PIXI.Application({
antialias: true, // 消除锯齿
width: 300,
height: 300
});

// 插入到 body 中
document.body.appendChild(app.view);

// 动画 https://www.codeandweb.com//demos/texturepacker/pixijs7-spritesheet-demo/index.html
// 动画制作教程 https://www.codeandweb.com/texturepacker/tutorials/how-to-create-sprite-sheets-and-animations-with-pixijs


await PIXI.Assets.load(['icon.jpg']);

let sprite1 = new PIXI.Sprite(PIXI.Assets.get("icon.jpg").clone());
sprite1.x = 10;
sprite1.y = 10;
app.stage.addChild(sprite1);

// 将 ticker.deltaTime 缩放到大约120 FPS
// app.ticker.speed = 2;

let sleep = 1;
// 放入游戏循环的任何代码,每秒都会被执行 60 次
app.ticker.add(delta => {
// delta = (当前帧的时间 - 上一帧的时间) / (1000 / 60); 其中1000是指1000ms,即1秒, 60是1秒种执行60次。 (1000 / 60)就是每秒钟执行60次,帧与帧之前的平均时间间隔。
//移动精灵 1 像素
if (sprite1.width + sprite1.x + 10 > app.renderer.width) {
sleep = -1;
} else if (sprite1.x < 10) {
sleep = 1;
}
sprite1.x += sleep;
});


// 使用独立的 ticker 控制动画
const ticker2 = PIXI.Ticker.shared;
let sprite2 = new PIXI.Sprite(PIXI.Assets.get("icon.jpg").clone());
sprite2.x = 10;
sprite2.y = 10 + 32 + 10;
// 使用速度属性(vx和vy)来控制精灵的移动速度
sprite2.vx = 1;
sprite2.vy = 1;
app.stage.addChild(sprite2);
ticker2.add(delta => {
if (sprite2.width + sprite2.x + 10 > app.renderer.width) {
// 触底判断
sprite2.vx = -1;
} else if (sprite2.x < 10) {
// 触顶判断
sprite2.vx = 1;
}
if (sprite2.height + sprite2.y + 10 > app.renderer.height) {
// 触底判断
sprite2.vy = -1;
} else if (sprite2.y < 10) {
// 触顶判断
sprite2.vy = 1;
}
sprite2.x += sprite2.vx;
sprite2.y += sprite2.vy;
});

document.querySelector('#stop_all').addEventListener('click', () => {
// 停止动画
app.ticker.stop();
})
document.querySelector('#start_all').addEventListener('click', () => {
// 开始动画
app.ticker.start();
})

document.querySelector('#stop_2').addEventListener('click', () => {
// 停止动画
ticker2.stop();
})
document.querySelector('#start_2').addEventListener('click', () => {
// 开始动画
ticker2.start();
})
}

init();

上诉代码利用了 PIXI.Ticker.shared 实现独立精灵动画的暂停与播放效果, app.ticker 可以控制整个场景动画停止,但是 PIXI.Ticker.shared 并不会停止,如果继续场景动画播放, PIXI.Ticker.shared 会跳过暂停的这段时间,直接跳到当前时间轴开始播放。具体效果可以点击上诉示例中的暂停、播放看看。

精灵表动画

本例中的资源来源于:https://www.codeandweb.com/demos/texturepacker/pixijs7-spritesheet-demo/index.html

以上代码可实现简单的动画移动,如果要实现动画场景中的人物动作,那么应该如何做呢?

官方推荐使用 TexturePacker 工具制作复杂动画,下载地址:https://www.codeandweb.com/texturepacker

TexturePacker 动画示例效果:

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pixijs入门示例-精灵表动画暂停与播放</title>
<script src="https://pixijs.download/v7.3.2/pixi.js"></script>
<style>
html, body, div { padding:0; margin: 0; }
body, canvas {
width:100%;
}
</style>
</head>
<body>
<div style="margin: 12px;">
<button id="stop_1">停止场景所有动画</button>
<button id="start_1">开始场景所有动画</button>
</div>
<div style="margin: 12px;">
<button id="stop_2">停止人物动画</button>
<button id="start_2">开始人物动画</button>
</div>
<script>
async function init() {
// 创建一个 pixi 实例
let app = new PIXI.Application({
antialias: true, // 消除锯齿
width: 960,
height: 540
});

// 插入到 body 中
document.body.appendChild(app.view);

let style = new PIXI.TextStyle({
fontFamily: "Futura",
fontSize: 24,
fill: "white",
align: "center",
});
let message = new PIXI.Text("资源加载中\r\n0%", style);
message.anchor.set(0.5, 0.5);
// 设置 message 坐标
message.x = app.renderer.width / 2;
message.y = app.renderer.height / 2;
app.stage.addChild(message);
await PIXI.Assets.load([
{ alias: 'spritesheet', src: 'example-spritesheet.json', },
{ alias: 'background', src: 'background.watermark.png', },
{ alias: 'middleground', src: 'middleground.watermark.png', },
], function(progress) {
// 获取加载进度
message.text = `资源加载中\r\n${parseInt(progress * 100)}%`
console.log(progress);
});

// 资源加载完成,销毁文本对象
message.destroy();

// 初始化背景
const background = PIXI.Sprite.from("background");
app.stage.addChild(background);

// 将舞台缩放到适合的大小
app.stage.scale.x = app.view.width / background.width;
app.stage.scale.y = app.view.height / background.height;

// 添加场景
const foreground = PIXI.Sprite.from("middleground");
app.stage.addChild(foreground);

// 获取解析动画所需的精灵表 json 数据
const animations = PIXI.Assets.get('spritesheet').data.animations;

// 创建动画精灵
const character = PIXI.AnimatedSprite.fromFrames(animations["example"]);

// configure + start animation:
character.animationSpeed = 1 / 6; // 动画速率 6 fps
character.position.set(150, background.height - 180); // 设置人物位置
character.play();

// 启用此选项可随每个动画帧更新锚点
// character.updateAnchor = true;

// 添加人物到舞台
app.stage.addChild(character);

// 让人物移动起来
app.ticker.add(delta => {
character.x = (character.x + 6 * delta) % (background.width + 200);
});


document.querySelector('#stop_1').addEventListener('click', () => {
// 停止动画
app.ticker.stop();
})
document.querySelector('#start_1').addEventListener('click', () => {
// 开始动画
app.ticker.start();
})

document.querySelector('#stop_2').addEventListener('click', () => {
// 停止动画
console.log(character);
character.stop();
})
document.querySelector('#start_2').addEventListener('click', () => {
// 开始动画
character.play();
})
}

init();

</script>
</body>
</html>

总结

复杂的游戏动画,需要设计师配合完成,简单的动画效果可以在程序中使用代码完成。

本文由 linx(544819896@qq.com) 创作,采用 CC BY 4.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。本文链接为: https://blog.jijian.link/2023-11-27/js-pixi-animation/

如果您觉得文章不错,可以点击文章中的广告支持一下!