一、简介
本系列是 RxJS 制作简单游戏教程,本文基于 React(Remix)/Canvas/RxJS 制作键盘控制box在水平和二维方向移动,移动方式有两种一种是键盘操作,一种是鼠标操作。
二、特点
- rxjs 事件流与canvas结合
- react(remix) 函数组件
- 在订阅位置绘制内容
- scan 操作符合并
- canvas 绘制到清除到重新绘制流程
三、三个任务
- 键盘控制一维水平移动
- 键盘控制二维移动
- 鼠标控制控制移动
四、键盘一维水平移动
目标:在画布中基于 React/RxJS 通过键盘的左右方向键控制 canvas 中的 box 盒子水平方向移动。
ts
import { useRef, useEffect } from "react";
import { fromEvent, merge } from "rxjs";
import { scan, filter, map } from "rxjs/operators";
const step = 10
const Game = () => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current! as HTMLCanvasElement;
const context = canvas.getContext("2d");
const leftKeyStream = fromEvent(document, "keydown").pipe(
filter((event) => event.key === "ArrowLeft"),
map(() => -step)
);
const rightKeyStream = fromEvent(document, "keydown").pipe(
filter((event) => event.key === "ArrowRight"),
map(() => step)
);
const moveStream = merge(leftKeyStream, rightKeyStream).pipe(
scan((x, dx) => Math.max(0, Math.min(400 - 50, x + dx)), 0)
);
const subscription = moveStream.subscribe((x) => {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillRect(x, 90, 50, 50);
});
return () => subscription.unsubscribe();
}, []);
return (
<div>
<h1>左右移动方块游戏 (RxJS)</h1>
<canvas ref={canvasRef} width={400} height={200} style={{ border: "1px solid black" }} />
</div>
);
};
export default Game;
使用 RxJS 实现了三个流:
- leftKeyStream
- rightKeyStream
- moveStream
moveStream 是将 leftKeyStream 和 rightKeyStream 合并而来。它的作用是通过 scan (与 js 的 reduce 功能基本一致) 操作计算出当前的水平的位置。moveStream 订阅之后执行清除画布,然后重新绘制内容。其中 step 是每次按下键盘移动步骤。在此组件汇总使用 RxJS 操作符有:
- fromEvent 用于捕获事件
- filter 过滤键盘事件
- map 映射步骤(正负)
- merge 合并左右移动留
此移动属于一维层面移动,相对简单,也 Canvas 动画的基础。
五、键盘二维移动
目标:在画布中基于 React/RxJS 通过键盘的左右方向键控制 canvas 中的 box 盒子水平方向和垂直方向进行移动。
ts
import { useRef, useEffect } from "react";
import { fromEvent, merge } from "rxjs";
import { filter, map, scan } from "rxjs/operators";
const step = 10;
const Game = () => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current! as HTMLCanvasElement;
const context = canvas.getContext("2d");
const leftKeyStream = fromEvent(document, "keydown").pipe(
filter((event) => event.key === "ArrowLeft"),
map(() => ({ x: -step, y: 0 }))
);
const rightKeyStream = fromEvent(document, "keydown").pipe(
filter((event) => event.key === "ArrowRight"),
map(() => ({ x: step, y: 0 }))
);
const upKeyStream = fromEvent(document, "keydown").pipe(
filter((event) => event.key === "ArrowUp"),
map(() => ({ x: 0, y: -step }))
);
const downKeyStream = fromEvent(document, "keydown").pipe(
filter((event) => event.key === "ArrowDown"),
map(() => ({ x: 0, y: step }))
);
const moveStream = merge(leftKeyStream, rightKeyStream, upKeyStream, downKeyStream).pipe(
scan((position, delta) => {
const x = Math.max(0, Math.min(400 - 50, position.x + delta.x));
const y = Math.max(0, Math.min(200 - 50, position.y + delta.y));
return { x, y };
}, { x: 0, y: 0 })
);
const subscription = moveStream.subscribe((position) => {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillRect(position.x, position.y, 50, 50);
});
return () => subscription.unsubscribe();
}, []);
return (
<div>
<h1>四方向移动方块游戏 (RxJS)</h1>
<canvas ref={canvasRef} width={400} height={200} style={{ border: "1px solid black" }} />
</div>
);
};
export default Game;
在二维空间之后,需要更多的访问进行支持 相对水平方向增加了:
- leftKeyStream
{ x: -step, y: 0 }
- rightKeyStream
{ x: step, y: 0 }
- upKeyStream
{ x: 0, y: -step }
- downKeyStream
{ x: 0, y: step }
将以上四个流合并之后,通过 scan 计算新的位置,然后清除画布,重新绘制。
六、鼠标控制移动
ts
import { useRef, useEffect } from "react";
import { fromEvent } from "rxjs";
import { map, scan } from "rxjs/operators";
const step = 10;
const Game = () => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current! as HTMLCanvasElement;
const context = canvas.getContext("2d");
const canvasRect = canvas.getBoundingClientRect();
const mouseMoveStream = fromEvent(canvas, "mousemove").pipe(
map((event) => {
return {
x: event.clientX - canvasRect.left - 25, // 25 is half of the square width
y: event.clientY - canvasRect.top - 25, // 25 is half of the square height
};
})
);
const moveStream = mouseMoveStream.pipe(
scan((position, target) => {
const deltaX = target.x - position.x;
const deltaY = target.y - position.y;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // 三角计算水平距离
if (distance <= step) {
return target; // 距离小于步距的时候,直接返回 target
}
const ratio = step / distance;
const x = position.x + deltaX * ratio;
const y = position.y + deltaY * ratio;
return { x, y };
}, { x: 0, y: 0 })
);
const subscription = moveStream.subscribe((position) => {
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillRect(position.x, position.y, 50, 50);
});
return () => subscription.unsubscribe();
}, []);
return (
<div>
<canvas ref={canvasRef} width={400} height={200} style={{ border: "1px solid black" }} />
</div>
);
};
export default Game;
逻辑与键盘相似,使用 fromEvent 操作符获取鼠标移动事件 mousemove
的获取 canvas 画布在浏览器中的位置,然后计算出 target 的位置。在管道之内 pipe 操作符计算出新的元素的位置。
七、小结
本主要是练习 Canvas + RxJS 在 React 框架中,实现基本移动操作操作,分为键盘移动,鼠标移动,使用 RxJS 作为计算工具如何处理。RxJS 是个强大的函数式和响应式编程工具,在计算上提供诸多操作符完成响应的任务。