小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
介绍
上一期,我们讲述了怎么对游戏的字体资源和图片资源怎么去压缩优化,本期就来具体实现那个犬夜叉被折断的铁碎牙这样小故事片段的案例。如图所示:
经过上次的操作,我们的字体资源和图片资源都有了,本次就会学习怎么利用这些资源在pixi.js去完成这个对白场景的制作,我们大致会从安装依赖,基础结构,资源加载,资源绘制,打字机效果,切换画面等方面入手去实现它,马上要开始咯~
实现篇
1.安装依赖
我们将用vite去搭建这个项目。
yarn add -D vite
复制代码
之后我们要再安装pixi.js
yarn add pixi.js
复制代码
2.基础结构
我们再新建index.html文件,里面写入基本样式,当然js引入要用module模式,因为后面要使用刚下载的pixi.js
<body>
<div id="app"></div>
<script type="module" src="./app.js"></script>
</body>
复制代码
* {
padding: 0;
margin: 0;
}
html,
body {
width: 100%;
height: 100vh;
position: relative;
overflow: hidden;
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
}
#app {
width: 800px;
height: 600px;
background-color: white;
}
复制代码
顺道将css也给写了,这里主要就是规定主容器的#app的大小,并让其居中显示。
接下来,我们要完成app.js基本结构了,这是我们的主逻辑。
/*app.js*/
import * as PIXI from "pixi.js"
import data from "./data"
class Application {
constructor() {
this.app = null; // PIXI.Application实例
this.stage = null; // 游戏舞台
this.talkStage = null; // 对白场景容器
this.w = 800; // 场景的宽
this.h = 600; // 场景的高
this.stageIndex = 0; // 场景当前下标
this.stageTextures = {}; // 场景资源
this.charTimer = []; // 打字机的timer集合
this.init();
}
init() {
// 初始化
let el = document.getElementById("app")
this.app = new PIXI.Application({
resizeTo: el,
backgroundColor: 0xFFFFFFF
});
this.w = this.app.view.width;
this.h = this.app.view.height;
this.stage = this.app.stage;
this.talkStage = new PIXI.Container();
this.stage.addChild(this.talkStage);
el.appendChild(this.app.view);
this.load();
this.bindEvent();
}
bindEvent() {
// 绑定事件
}
changeStageIndex() {
// 切换场景
}
render() {
// 渲染
this.drawBgImg();
this.drawTalk();
}
load() {
// 加载
}
drawBgImg() {
// 绘制背景图
}
drawTalk() {
// 绘制对白
this.drawContent();
}
drawContent() {
// 绘制对白内容
}
}
window.onload = new Application();
复制代码
我们在主逻辑中主要做了这几点:
-
引入了pixi.js和用到的脚本数据data.js。pixi.js将作为我的引擎贯穿全程。而data是我们自己写的对话脚本数据。如下图,就是我们对话中每张的场景画面和对话的人物和内容都在这里写入:
-
其次是我们执行初始化事件,实例化PIXI.Application,其中为了直接填满#app容器,我们可以直接使用resizeTo(el)方法去直接填充,而不用通过宽高去设置。最后别忘了用el.appendChild(app.view)再将画布填充到#app容器里达成显示。
-
app.stage是游戏的舞台,里面可以放置要渲染的内容,但是我们期望每个场景都会有一个舞台方便去控制,所以PIXI.Container实例化的talkStage作为对白场景的主容器去添加到主场景app.stage里面。后面所有的对白场景的内容和图片都会追加到talkStage里面。
3.资源加载
我们之前已经完成了资源的准备工作,在load里面可以通过Promise+loader在初始化加载完直接去保存好我们用的资源数据,然后就可以进行渲染了。
init(){
// ...
this.load().then(res => {
this.stageTextures = res.resources.stage.textures;
this.render();
})
// ...
}
load() {
return new Promise((resolve, reject) => {
this.app.loader
.add('hk', 'font/hk/font.fnt')
.add("stage", "assets/stage.json")
.load(resolve);
})
}
复制代码
4.资源绘制
drawBgImg() {
const {stageIndex, stageTextures, talkStage} = this;
let _img = data[stageIndex].img;
if (!_img) return;
let img = stageTextures[_img + ".png"];
let bgImg = new PIXI.Sprite(img);
bgImg.anchor.set(0, 0);
bgImg.position.set(0, -5)
bgImg.scale.set(1.23)
talkStage.addChild(bgImg);
}
drawTalk() {
const {stageIndex, stageTextures, stage} = this;
let _talk = data[stageIndex] ? data[stageIndex].talk : [];
if (!_talk || !_talk.length) return;
this.drawContent(_talk);
}
drawContent(res) {
const contentContainer = new PIXI.Container();
contentContainer.position.set(30, this.h - 210);
contentContainer.width = this.w - 120
this.talkStage.addChild(contentContainer);
const box = new PIXI.Graphics();
box.beginFill(0x000000);
box.drawRect(0, 0, this.w - 60, 180);
box.endFill();
box.alpha = 0.61;
contentContainer.addChild(box);
const nameText = new PIXI.BitmapText(res[0].name + ":", {
fontName: '华康少女字体',
fontSize: 32,
tint: 0xFFFFFF,
letterSpacing: 5,
textHeight: 32,
align: "left",
});
nameText.position.set(30, 15)
contentContainer.addChild(nameText);
const contentText = new PIXI.BitmapText(res[0].content, {
fontName: '华康少女字体',
fontSize: 32,
tint: 0xFFFFFF,
letterSpacing: 3,
textHeight: 120,
textWidth: this.w - 120,
align: "left",
maxWidth: this.w - 120
});
contentText.position.set(30, 55)
this.talkStage.addChild(contentText);
contentContainer.addChild(contentText);
}
复制代码
看似很多,大部分都是繁琐重复的业务,和定位,嵌套。就记住两点:
- 图片用PIXI.Sprite传入相应的纹理数据实现绘制
- 位图文字用PIXI.BitmapText传入要要绘制的文字和字体及一些字体属性实现绘制
这样第一个场景界面就完成了~
5.打字机效果
如果我们经常玩日式RPG会发现很多对白是用打字机效果逐个实现的,非常有代入感。我们将用短短几行来实现他。
drawContent(res) {
// ...
const contentText = new PIXI.BitmapText("", {
fontName: '华康少女字体',
fontSize: 32,
tint: 0xFFFFFF,
letterSpacing: 3,
textHeight: 120,
textWidth: this.w - 120,
align: "left",
maxWidth: this.w - 120
});
Array.from(res[0].content).forEach((char, i) => {
(function(i) {
let timer = setTimeout(() => {
contentText.text += char;
}, 200 * (i + 1))
this.charTimer.push(timer)
}.bind(this))(i)
})
// ...
}
复制代码
我们要改造下drawContent方法,将刚才的内容直接变成空字符串,然后遍历内容,将其每个200毫秒增加一个字就形成了打字机的效果。并且我们还要把每个字的timer存起来,如果没打完字就切换下一张那么我们就会把timer停止删除掉。
render() {
if (this.charTimer.length) {
this.charTimer.forEach(timer=>{
clearTimeout(timer)
})
this.charTimer.length = 0;
};
this.talkStage.children.forEach((c, i) => {
this.talkStage.removeChildAt(i)
})
this.drawBgImg();
this.drawTalk();
}
复制代码
我们会在charTimer中逐个删除清空timer,而且我们同时保证后面切换场景要把原场景的元素也清空。
6.切换画面
终于来到最后一步了,我们先绑定一下点击事件,然后点击画面会切到下个图片和文字。
bindEvent() {
this.talkStage.interactive = true;
this.talkStage.buttonMode = true;
this.talkStage.on("pointerdown", this.changeStageIndex.bind(this));
}
changeStageIndex() {
if (this.stageIndex >= data.length - 1) return;
this.stageIndex += 1;
this.render();
}
复制代码
我们用pointerdown点击事件绑定好切换场景的方法,然后如果当前对白数据没执行完的话让他stageIndex下标加一,然后渲染出来的就是新画面了。就是这么简单,而且如果超过的话,我们在这里是给他中断了,实际可能跳入其他逻辑,比如战斗,小游戏,其他对白等等,要看策划产出的具体业务了。
结语
在次我们就完全实现了一个简单的游戏的对白场景动画,我们可以在此基础上继续丰富他的体验和功能,但核心就只有这些。虽然仅仅是个对白场景动画,但我们可以认识到资源的加载渲染,状态的改变,事件的交互一款游戏都逃不过这三条,再此基础上又可以做动画比如刚刚的打字机,了了几句代码就可以增强游戏代入体验。还等什么我们发挥我们的想想,来造一款属于我们自己的故事吧~