这是多篇系列博文中的第 5 篇。之前的博文可在此处找到:
- 在以下位置构建小部件 Mendix 使用 React — 第一部分 — 颜色计数器
- 在中构建小部件 Mendix 使用 React 第二部分 — 计时器
- 在中构建小部件 Mendix 使用 React 第 3 部分 — Kanban
- 在以下位置构建小部件 Mendix 使用 React 第 4 部分 - ArcGIS Maps
我们正在建设什么
生命的游戏!不,不是 经典的棋盘游戏… 而是一个细胞自动化单人游戏。通过一些简单的规则,你可以 构建一个由活细胞和死细胞组成的宇宙 创造出很酷的、重复的图案。

在我们开始之前——Wasm 到底是什么鬼?
因此,我们将构建一款游戏,其中我们将利用用 Rust 编写并编译为 WebAssembly 的代码。

但是什么是 WebAssembly?它是一个小型、快速、高效的基于堆栈的虚拟机,允许您执行用 C、C++、Rust、Python 或其他语言编写的代码编译的字节码。
但这实际上意味着什么?
好吧,WebAssembly 本质上被编译为二进制格式,这种格式加载起来更小,运行速度也更快。您还可以利用流行编程语言的库,并将现有代码转换为可在浏览器中运行的代码, 就像《毁灭战士 3》.

这对于游戏来说很酷,但实际上有人会使用它吗?
答案是肯定的。大型 Web 应用如 FIGMA 和 Zoom 大多使用 WebAssembly 编写。但除此之外,一大批有用的 npm 包都利用了 .wasm(编译的 WebAssembly)文件, 包括我们在最近的博客中使用的 ArcGis 库.
WebAssembly 入门
创建 WebAssembly 文件的方法有很多种,包括使用以下方式编译代码 脚本。对于这个例子,我们将使用用 Rust 编写的代码。
要用 WebAssembly 编写生命游戏并将其发布为节点包,我们可以在小部件中轻松使用, 我跟着这个教程。本教程是开始使用 WebAssembly 的绝佳资源,如果您想熟悉 Rust 和 WebAssembly,我强烈推荐它。
本教程的输出是一个节点库,由一个 Wasm 文件组成,其中包含我们编译的 Rust 代码、一个定义我们与 Wasm 文件的接口的 javascript 文件以及一个用于将我们的代码与 Typescript 一起使用的类型文件。
一旦我们有了 npm 库,我们就可以在 Mendix 使用可插入小部件。
小部件时间
我们首先搭建我们的小部件 yo @mendix/widget gameOfLife 并重命名我们的组件。然后我们导入我们的 WebAssembly npm 库 npm i wasm-game-of-life-joerob319 (或者您自己创建的图书馆)。
从我们的子组件开始,我们可以开始使用 Wasm 文件。为此,我们需要导入我们的文件并使用 npm 库中提供的类型:
import * as wasm from "wasm-game-of-life-joerob319";
import { Universe, Cell, wasm_memory } from "wasm-game-of-life-joerob319";
接下来,让我们在组件在 useEffect 中首次渲染时加载文件并创建单元格 Universe。
const initiateWasm = async () => {
wasm.default().then(() => {
setUniverse(wasm.Universe.new());
});
};
useEffect(() => {
initiateWasm();
}, []);
渲染
我们的 WebAssembly 文件现已加载。我们需要考虑如何在页面上显示 Universe。
首先要计算宇宙的像素大小。幸运的是,我们可以从 WebAssembly 中获取宇宙的宽度和高度(以单元格数量表示),然后将其存储在状态中,如下所示:
const [width, setWidth] = useState<number>();
const [height, setHeight] = useState<number>();
useEffect(() => {
if (universe) {
setWidth(universe!.width());
setHeight(universe!.height());
}
}, [universe]);</number></number>
我们希望在屏幕上显示我们的宇宙。为此,我们将利用 画布 – 一个强大的工具,允许您使用 Javascript 进行绘图。
return (
<div>
<canvas ref="{canvasRef}"></canvas>
</div>
);
然后,我们更新 useEffect 来设置画布的高度和宽度。为此,我们需要使用画布引用并定义 CellSize。
const canvasRef = useRef(null)
const CellSize = 5;
useEffect(() => {
if (universe) {
setWidth(universe.width());
setHeight(universe.height());
const canvas = canvasRef.current!;
if (universe.height()) {
canvas.height = universe.height() * (CellSize + 1);
setHeight(universe.height());
}
if (universe.width()) {
canvas.width = universe.width() * (CellSize + 1);
setWidth(universe.width());
}
}
}, [universe]);
是时候画一些漂亮的图画了!让我们从网格开始:
const GridColour = ‘#CCCCCC’
const drawGrid = () => {
const canvas = canvasRef.current!;
const ctx = canvas.getContext("2d")!;
ctx.beginPath();
ctx.strokeStyle = GridColour;
// Vertical lines.
for (let i = 0; i <= width!; i++) {
ctx.moveTo(i * (CellSize + 1) + 1, 0);
ctx.lineTo(i * (CellSize + 1) + 1, (CellSize + 1) * height! + 1);
}
// Horizontal lines.
for (let j = 0; j <= height!; j++) {
ctx.moveTo(0, j * (CellSize + 1) + 1);
ctx.lineTo((CellSize + 1) * width! + 1, j * (CellSize + 1) + 1);
}
ctx.stroke();
};
下一步是绘制细胞。在我们的 WebAssembly 文件中,细胞被存储为活着的或死去的。为了在我们的小部件中显示它们,我们需要访问 Wasm 内存。
关于 Wasm 内存,有一点很重要,那就是它是线性的。这意味着我们可以在 JavaScript 中以字节数组(无符号 8 位整数)的形式访问它。
因此,我们从内存缓冲区创建一个数组,从 Wasm 告诉我们当前单元所在的位置开始,然后取宇宙中所有单元的长度,该长度等于单元高数乘以单元宽数。
const cellsPtr = universe!.cells();
const memory = wasm_memory();
const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);
然后可以将其转换到我们的画布上,如下所示:
const getIndex = (row: number, column: number) => {
return row * width! + column;
};
const DeadColour = ‘#FFFFF’
const AliveColour - ‘#3a34eb’
const drawCells = () => {
const cellsPtr = universe!.cells();
const memory = wasm_memory();
const cells = new Uint8Array(memory.buffer, cellsPtr, width! * height!);
console.log (cellsPtr)
console.log (cells);
const canvas = canvasRef.current!;
const ctx = canvas.getContext("2d")!;
ctx.beginPath();
for (let row = 0; row < height!; row++) {
for (let col = 0; col < width!; col++) {
const idx = getIndex(row, col);
ctx.fillStyle = cells[idx] === Cell.Dead ? DeadColour : AliveColour;
ctx.fillRect(col * (CellSize + 1) + 1, row * (CellSize + 1) + 1, CellSize, CellSize);
}
}
ctx.stroke();
};
我们使用 getIndex 将行和列转换为由 Wasm 内存创建的 1D 数组中的位置。然后根据每个方块是活着还是死去,用适当的颜色填充每个方块。
动画
现在我们可以开始为小部件制作动画了。我们希望让用户停止和启动动画。
const [isPaused, setIsPaused] = useState(true);
const btnClick = () => {
setIsPaused(prevState => !prevState);
};
return (
<div>
<canvas ref={canvasRef} />
<div className="btnContainer">
<button onClick={btnClick} className="btn mx-button">
{isPaused ? "▶" : "⏸"}
</button>
</div>
</div>
);
为了制作动画,我们将创建一个渲染循环,利用 请求动画帧() 创建一个循环来为网格添加动画效果。我们使用 宇宙.tick() 函数重新计算所有单元格,然后绘制画布。
let animationId: number | null = null;
const renderLoop = () => {
universe!.tick();
drawGrid();
drawCells();
animationId = requestAnimationFrame(renderLoop);
};
然后我们使用 useLayoutEffect 以便更新 在浏览器绘制屏幕之前.
useLayoutEffect(() => {
if (!isPaused) {
renderLoop();
return () => cancelAnimationFrame(animationId!);
} else {
if (animationId) {
cancelAnimationFrame(animationId!);
}
}
}, [isPaused]);
然后我们包括我们的 取消动画帧 在我们的清理过程中避免任何副作用。我们运行应用程序并单击按钮...错误!
汇总汇总
这是因为我们还没有真正将 Wasm 文件加载到浏览器中运行。为此, 我们需要我们的老朋友 rollup.
我们跑 npm i rollup-plugin-copy-save 在我们的根目录中创建我们的汇总文件:
import copy from "rollup-plugin-copy";
export default args => {
const result = args.configDefaultConfig;
return result.map((config, index) => {
if (index === 0) {
const plugins = config.plugins || []
config.plugins = [
...plugins,
copy({
targets: [{ src: "node_modules/wasm-game-of-life-joerob319/*.wasm", dest: "dist/tmp/widgets/mendix/gameofLife/" }]
}) ]
}
return config;
});
};
复制插件移动我们的 Wasm 文件以确保它是我们小部件包的一部分并将提供给浏览器。
如果我们运行我们的小部件,我们就有了我们的人生游戏。
只剩下两件小事要做……
杀死或复活你的细胞
所以我们的“生命游戏”目前很酷,但用户无法设置宇宙的状态。让我们让用户可以点击来切换细胞的存活或死亡。
为此,我们创建了一个名为 添加点击点,然后通过查找画布左上角的位置和单击位置来计算单击了哪个单元格,确保将其乘以应用于画布的任何比例。一旦我们有了行和列,我们就可以使用 切换单元格 我们的 webassembly 文件中的函数来更新宇宙,然后重新绘制画布。
const addPointClick = (event: React.MouseEvent): void => {
event.preventDefault();
if (canvasRef.current!) {
const node = canvasRef.current;
const boundingRect = node.getBoundingClientRect();
const scaleX = node.width! / boundingRect.width;
const scaleY = node.height! / boundingRect.height!;
const canvasLeft = (event.clientX - boundingRect.left) * scaleX;
const canvasTop = (event.clientY - boundingRect.top) * scaleY;
const row = Math.min(Math.floor(canvasTop / (CellSize + 1)), height! - 1);
const col = Math.min(Math.floor(canvasLeft / (CellSize + 1)), width! - 1);
universe!.toggle_cell(row, col);
drawGrid();
drawCells();
}
};
确保也将其添加到画布中:
<canvas ref={canvasRef} onClick={addPointClick}/>
瞧...它活了!


整理
最后,让我们从文件 CellSize、GridColour、DeadColour、AliveColour 中删除变量,然后允许小部件用户设置它们。将 xml 更新为:
<propertyGroup caption="General">
<property key="CellSize" type="integer" required="true" defaultValue="5">
<caption>Cell Size</caption>
<description>Size of each square in px</description>
</property>
<property key="GridColour" type="string" required="true" defaultValue="#CCCCCC">
<caption>Grid Colour</caption>
<description>Colour of Grid</description>
</property>
<property key="DeadColour" type="string" required="true" defaultValue="#FFFFFF">
<caption>Dead Colour</caption>
<description>Colour of Dead Cells</description>
</property>
<property key="AliveColour" type="string" required="true" defaultValue="#000000">
<caption>Alive Colour</caption>
<description>Colour of Alive Cells</description>
</property>
</propertyGroup>
你的最终父母应该是这样的:
export function GameOfLife({ CellSize, GridColour, DeadColour, AliveColour }: GameOfLifeContainerProps): ReactElement {
return (
<GameOfLifeComponent
CellSize={CellSize}
GridColour={GridColour}
DeadColour={DeadColour}
AliveColour={AliveColour}
/>
);
}
并更新子项以接受它们作为参数(不要忘记从代码中删除现有变量!)。
export interface GameOfLifeComponentProps {
CellSize: number;
GridColour: string;
DeadColour: string;
AliveColour: string;
}
export function GameOfLifeComponent({
CellSize,
GridColour,
DeadColour,
AliveColour
}: GameOfLifeComponentProps): ReactElement {
…..
}
好了,我们完成了!我们已经了解了如何用 C、C++、Rust 或 Python 编写代码并在你的 Mendix 应用程序。与以往一样,您可以在 Github 上找到该小部件的完整代码: GitHub – joe-robertson-mx/gameOfLife.
结局……好吧,并非如此
这就是可插入小部件系列的结尾!我们从创建一个简单的计数器小部件开始,一直到在我们的 Mendix 应用程序。
这并不是说这是结束……在接下来的几周和几个月里,还会有更多精彩的小部件内容出现。如果您有任何想看的内容或对本系列有任何反馈,只需在本故事下留言即可。