写在前面
印象中第一次看NBA是在1999年的夏天吧,当跟着学校校队巡演招生什么的,住在一个破宾馆里,一群人围着一台黑白电视机,当时还小,不理解为什么队里的大哥哥们看个比赛鬼吼鬼叫上蹿下跳的。记得那天是公牛打76人,乔丹对位艾佛森。
2002年姚明当选NBA的状元秀,开始关注NBA、姚明和休斯顿火箭队。真正开始看火箭队的比赛是上高中的时候吧,诺瓦克绝杀,22连胜,季后赛魔咒,整个高中时期的记忆,不是考试,不是校花,而是藏在一堆课本下的摩托罗拉,3G手机门户的NBA文字直播,和主播阿星的“BOOOOOOOM BABY!!!”。
转眼10几年年就过去了,工作后已经很久都没有机会完整的看一场比赛了,大年初二在朋友圈看到科比离开的消息一直不敢相信,刷了一天的新闻。
感觉青春,和一个时代就这么终结了。
Keep going
由于疫情出不了门,无意间又翻到了一个朋友在科比退役时画的图
人气插画师:冬眠的熊 作品(lofter:xiangxdx.lofter.com/、weibo:weibo.com/u/217898786…)
于是想写一个简单的投篮游戏致敬一下,找朋友重新画了一版素材
这就是我们今天要做的游戏:
游戏原理
首先我们了解一下物理游戏的开发原理
一个物理游戏是由2个部分组成的:
物理计算通常我们需要借助一些现成的物理引擎,如Box2d
、P2.js
、matter.js
等
图形渲染可以用原生canvas,也可以用更强大的2d渲染引擎Pixi.js、CreateJS等
物理引擎通常只负责模拟物理运动计算,而不涉及图形渲染
以此减小包的体积,同时更灵活地结合不同渲染方式完成游戏开发
单纯使用物理引擎,你在界面上是看不到任何东西的
物理引擎实际上跟大家常用的缓动函数计算库Tween.js是一类东西
基本概念
我们选择MatterJS + PixiJS组合来实现这个游戏
MattterJS:brm.io/matter-js/
Pixi.js:pixijs.io/
两个库的使用方法参照官方的示例DEMO,这里就不详细介绍了
了解几个基本的概念就可以开始进行游戏开发了
Matter会创建一个叫World
的东西,用来模拟仿真物理世界,添加到World中的物体,就具备了重力、摩擦力、碰撞等物理属性,这些物体我们称之为Rigidbody(
刚体),在Matter中,刚体分为2种,动态刚体(Dynamic Body)与静态刚体(Static body)
PIXI会创建一个叫Stage
的东西,用于图形渲染,实体就是DOM中的canvas
节点,而添加到画布里的图形,我们称之为Sprite
(贴图)
物理游戏就是由物理引擎(虚拟环境)到渲染引擎(可视环境)的映射
需要确保物理引擎与渲染引擎使用同一坐标系,有些物理引擎(如P2)与Pixi坐标系不同,要先进行全局的坐标变换
开始开发
初始化物理环境与渲染环境
var Engine = Matter.Engine.create()
var world = Engine.world // 物理环境
var canvas = document.getElementById('canvas')
var App = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
transparent: true
})
var stage = App.stage // 渲染环境
canvas.appendChild(App.view)
App.ticker.add((delta) => {
// update() // 启动刷新器
});
前面提到了物理引擎通常不负责图形渲染,而Matter提供了一个简单的渲染器用于调试,让我们在没有接入渲染引擎时也能方便的以线框的形式看到物理引擎创建的刚体
var Render = Matter.Render
var render = Render.create({
element: document.getElementById('physic'),
engine: Engine,
options: {
width: window.innerWidth, height: window.innerHeight
}
})
创建一个圆形刚体
var body = Bodies.circle(x, y, radius, {
restitution: 0.7, // 弹性系数
density:0.05, // 密度
firction: 1 // 摩擦力
})
添加到World
中
World.add(world, body)
启动引擎与渲染器
Matter.Engine.run(engine)
Matter.Render.run(render)
这里我们就完成了物理引擎与渲染引擎的初始化,并且向物理世界中添加了第一个圆形刚体DEMO:codepen.io/guowc/pen/W…
创建贴图
用PIXI创建一个篮球贴图
var sprite = PIXI.Sprite.from('...ball.png');
sprite.anchor.set(.5) // 改变锚点至中心位置,方便定位
stage.addChild(sprite) // 贴图添加到渲染环境
刚体与贴图同步
在刷新器中同步绑定贴图与刚体位置、旋转角度
function update () {
sprite.position.x = body.position.x
sprite.position.y = body.position.y
sprite.rotation = body.angle
}
完成同步后,就可以把刚体的测试线框去掉了
var body = Bodies.circle(x, y, radius, {
...
render: { visible: false } // 关闭线框渲染
})
创建篮框
虽然我们的素材有一点点的透视,但我们实现的是2D视角,所以篮框的碰撞区域只需要在横切面设置2个静态刚体即可
这里注意把篮框拆为2部分,让篮球可以从篮框中间穿过
添加篮网
Matter中有一些预设的复合体挂载在Composites
中,比如softbody(软体)、car(汽车)、bridge(桥)等,我们用softbody来模拟篮网
// 参数说明参照 https://brm.io/matter-js/docs/classes/Composites.html
var nets = Composites.softBody(800, 240, 8, 5, 0, 0, false, 3.2, {
firction: 1,
frictionAir: 0.08,
render: { visible: false },
collisionFilter: { group: Body.nextGroup(true) }
}, {
render: { lineWidth: 2, strokeStyle: "#fff" },
stiffness: 1.4
})
发射篮球
给篮球设置一个线速度与角速度
// 操作刚体的方法挂载在Matter.Body类上
Body.setVelocity(body, { x: 1, y: -1 }); // 设置线速度
Body.setAngularVelocity(body, -0.1); // 设置角速度,使球轻微后旋
到这里一个简单的2D投篮游戏DEMO就完成了
加入人物
接下来我们用动画软件的骨骼系统制作一个科比的投篮动画
具体动画的制作过程这里就不详细介绍了,做的比较渣
接着导出png序列图
拉到TexturePack里合成精灵图,选择PIXI框架格式导出,得到一个精灵图和JSON配置文件
PIXI加载精灵图
App.loader.add('.../kobe.json').load(function(){
const frames = [];
for (let i = 1; i <= 37; i++) {
const val = i < 10 ? `0${i}` : i;
frames.push(PIXI.Texture.from(`kobe00${val}.png`));
}
var Kobe = new PIXI.AnimatedSprite(frames);
Kobe.animationSpeed = 0.4;
Kobe.loop = false
})
通过Kobe.play()
播放动画
关键问题
写到这里的时候遇到了一个问题,持球到起跳出手的过程,篮球是一个跟随手掌写死的动画,而投出后又是一个动态刚体,这两部分虚实如何衔接?
一开始我想到的方案是,动画序列里不包含篮球,只有一个空投的动作,创建一个篮球刚体跟随投篮动画中手掌的位置,尝试后发现很难同步,持球过程不是一个简单的线性轨迹,投篮动画的播放速率与Tween控制的篮球运动速率也是不可控的
第二个方案是在动画序列中完成前半部分持球跳投动作,在篮球投出瞬间留出一帧空白帧,在篮球消失位置瞬间创建一个一摸一样的篮球刚体,以此实现虚实衔接
关键代码
Kobe.onFrameChange = (e) => {
// 动画帧回调,e为当前播放帧索引
if (e === 4) {
// 第5帧篮球消失瞬间,创建刚体
var body = Bodies.circle(...)
World.add(world, body)
Body.setVelocity(body, { x: 10, y: -10 }); // 创建瞬间将球投出
...
}
}
最后
Heros comes and go, but legends are forever.