一起用代码吸猫!本文正在参与【喵星人征文活动】
使用React和Matter.js做了一个简单的横版猫咪快跑的跳跃游戏,Neko Jump。
可以通过这个链接试玩: Neko Jump (game-neko-jump.vercel.app)
React是前端用于构建web页面的一个很常见的库,Matter.js是web端一个很强大的物理引擎,主要通过canvas来绘制对象。
游戏设计
猫咪(Neko)向前跑,跳跃躲避各种障碍物向前跑动,并尽可能跑的更远,拿到更高的分数,主要通过两个不同力度等级的按钮来跳跃,如果发现自己过于靠后,猫咪会往前追赶,如果遇到障碍没有跳过,落后了,就会输。
技术实现
猫咪使用一个长方形的刚体,覆盖一个贴图来实现。
实际上为了方便实现,场景并没有移动,主要是随机生成的障碍物设置了static=true,然后向后移动。
其中比较关键的就是在React中使用Matter.js这类并没有专门为React适配的库,需要使用useRef这个不常用的hook api来对对象进行托管。
其他就是通过Matter.js实现游戏的逻辑,技术上比如实现static物体的移动等,Matter.js的Example:matter-js/manipulation.js at master · liabru/matter-js (github.com)有很好的展示。
仓库地址:Kingfish404/game-neko-jump: A Game Power by react and matter.js (github.com)
核心代码:
function App() {
const scene = useRef(null);
const engine = useRef(Engine.create());
const player = useRef(Body.create({}));
const [isStart, setIsStart] = useState(false);
const [isDead, setIsDead] = useState(false);
const [score, setScore] = useState(0)
useEffect(() => {
// mount
const cw = document.body.clientWidth;
const ch = document.body.clientHeight / 2;
const e = engine.current;
let elem: HTMLElement = document.body;
if (scene.current) {
elem = scene.current;
} else {
return () => { };
}
const render = Render.create({
element: elem,
engine: e,
options: {
width: cw,
height: ch,
background: cloudImage,
wireframes: false,
}
});
// boundaries
Composite.add(e.world, [
// top
Bodies.rectangle(cw / 2, 10, cw, 20, {
isStatic: true,
render: {
fillStyle: 'black'
}
}),
// left
Bodies.rectangle(-50, ch / 2, 5, ch, {
isStatic: true,
render: {
fillStyle: 'black'
}
}),
// buttom
Bodies.rectangle(cw / 2, ch - 10, cw * 2, 20, {
isStatic: true,
render: {
fillStyle: 'black'
}
})
]);
player.current = Bodies.rectangle(cw / 2,ch / 2,10,20,
{
render: {
fillStyle: 'white',
sprite: {
texture: catImage,
xScale: 0.05,
yScale: 0.05,
}
},
friction: 0,
frictionStatic: 0,
frictionAir: 0,
}
);
Render.run(render);
Composite.add(e.world, [player.current as Body]);
const obstacles: Array<Body> = [];
const map: Array<number> = [ch - 25, ch - 25, ch / 3, ch / 2, ch / 1.5];
const gameloop = setInterval(() => {
const index: number = Math.floor(Math.random() * map.length);
const box = Bodies.rectangle(
cw + 100,
map[index]
, 100, 20, {
isStatic: true,
friction: 0,
frictionStatic: 0,
frictionAir: 0,
});
obstacles.push(box);
Body.setVelocity(box, { x: -100, y: 0 });
Composite.add(e.world, [box])
Body.setVelocity(player.current, { x: player.current.velocity.x / 10, y: player.current.velocity.y })
if (player.current.position.x < 0) {
setIsStart(false);
setIsDead(true);
}
if (player.current.position.x < ch / 2) {
Body.setVelocity(player.current, { x: 1, y: 0 });
}
}, 700);
if (!isStart) {
clearInterval(gameloop);
}
Events.on(e, 'beforeUpdate', () => {
for (let i of obstacles) {
if (i.position.x < -50) {
Composite.remove(e.world, i);
} else {
Body.setVelocity(i, { x: -1, y: 0 })
Body.setPosition(i, { x: i.position.x - 1, y: i.position.y });
}
}
while (obstacles.length > 0 && obstacles[0].position.x < -50) {
obstacles.splice(0, 1);
}
Body.setAngularVelocity(player.current, 0);
});
// unmount
return () => {
// destroy Matter
Render.stop(render);
Engine.clear(e);
World.clear(e.world, false);
Composite.clear(e.world, false);
render.canvas.remove();
render.textures = {};
clearInterval(gameloop);
}
}, [isStart]);
useEffect(() => {
let id: any;
if (isStart) {
id = setInterval(() => {
setScore(score => { return score + 1 });
}, 1000);
}
return () => { clearInterval(id) }
}, [isStart])
const wait = 500;
const handleTinyDown = _.throttle((e: { clientX: number; clientY: number; }) => {
const p = player.current;
Body.applyForce(p, { x: p.position.x, y: p.position.y }, { x: 0, y: -0.006 });
}, wait)
const handleStrongDown = _.throttle((e: { clientX: number; clientY: number; }) => {
const p = player.current;
Body.applyForce(p, { x: p.position.x, y: p.position.y }, { x: 0, y: -0.008 });
}, wait)
return (
<div className="App">
<div className="score">{score}</div>
<div
className="playground"
ref={scene}
/>
<div className="control">
{isStart ?
<>
<div
className="btn"
onMouseDown={handleTinyDown}
>Jump</div>
<div
className="btn"
onMouseDown={handleStrongDown}
>Jump Jump</div>
</>
:
<>
{isDead ?
<div className="deadLogo">You Dead</div>
:
<div className="btn"
onClick={() => {
// run the engine
Runner.run(engine.current);
setIsStart(true);
}}
>Start</div>}
</>}
</div>
</div >
);
}
export default App;