これは複数回にわたるシリーズの第 5 回目のブログです。以前のブログは、こちらでご覧いただけます。
- ウィジェットを構築する Mendix React を使う — パート 1 — カラー カウンター
- ウィジェットを構築する Mendix React パート 2 — タイマー
- ウィジェットを構築する Mendix React パート 3 — カンバン
- ウィジェットを構築する Mendix React パート 4 — ArcGIS マップ
私たちが構築しているもの
人生ゲーム!いや、 古典的なボードゲーム…セルラーオートメーションの1人用ゲームです。いくつかの簡単なルールで、 生きている細胞と死んでいる細胞の宇宙を構築する クールな繰り返しパターンを作成します。

始める前に — 一体何だったのか‽
そこで、Rust で記述され、WebAssembly にコンパイルされたコードを活用するゲームを構築します。

しかし、WebAssembly とは何でしょうか? WebAssembly は、C、C++、Rust、Python などで記述されたコードからコンパイルされたバイトコードを実行できる、小型で高速、効率的なスタックベースの仮想マシンです。
しかし、これは実際何を意味するのでしょうか?
WebAssemblyは基本的にバイナリ形式にコンパイルされており、読み込みが小さく、実行が高速です。また、一般的なプログラミング言語のライブラリを活用して、既存のコードをブラウザで動作するように変換することもできます。 ドゥーム3のような.

これはゲームには最適ですが、実際にこれを使用している人はいるのでしょうか?
簡単に答えると、イエスです。大規模なウェブアプリは figma Zoomは主にWebAssemblyで書かれています。しかし、これ以外にも、多くの便利なnpmパッケージが.wasm(コンパイルされたWebAssembly)ファイルを活用しています。 最新のブログで使用したArcGisライブラリを含む.
WebAssembly を使い始める
WebAssemblyファイルを作成する方法は様々あり、例えば、 サインアップこの例では、Rust で記述したコードを使用します。
WebAssemblyでライフゲームを書いて、ウィジェットで簡単に使用できるノードパッケージとして公開するには、 私はこのチュートリアルに従いましたこのチュートリアルは、WebAssembly を使い始めるための優れたリソースであり、Rust と WebAssembly に慣れたい場合は、ぜひお勧めします。
このチュートリアルの出力は、コンパイルされた Rust コードを含む Wasm ファイル、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 メモリについて理解しておくべき重要なことの 8 つは、それが線形であるということです。つまり、JavaScript では基本的にバイト配列 (符号なし XNUMX ビット整数) としてアクセスできます。
そこで、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>
);
アニメーション化するには、 requestAnimationFrame() グリッドをアニメーション化するためのループを作成します。 ユニバース.ティック() 関数を使用してすべてのセルを再計算し、キャンバスを描画します。
let animationId: number | null = null;
const renderLoop = () => {
universe!.tick();
drawGrid();
drawCells();
animationId = requestAnimationFrame(renderLoop);
};
これを次のように呼び出します レイアウト効果を使用する 更新されるように ブラウザが画面を描画する前に.
useLayoutEffect(() => {
if (!isPaused) {
renderLoop();
return () => cancelAnimationFrame(animationId!);
} else {
if (animationId) {
cancelAnimationFrame(animationId!);
}
}
}, [isPaused]);
次に、 アニメーションフレームをキャンセル 副作用を避けるためにクリーンアップを行います。アプリケーションを実行してボタンをクリックすると…エラーが発生します。
ロールアップラウンドアップ
これは、実際にブラウザにWasmファイルをロードして実行していないためです。 私たちには古い友人のロールアップが必要です.
私たちは走る 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 ファイルを移動して、それがウィジェット パッケージの一部となり、ブラウザーに提供されるようにします。
ウィジェットを実行すると、人生ゲームが始まります。
やるべき小さなことはあと 2 つだけです...
細胞を殺すか蘇らせるか
現時点では、私たちのライフ ゲームは素晴らしいのですが、ユーザーが宇宙の状態を設定する機能はありません。ユーザーがクリックしてセルの生死を切り替えられるようにしてみましょう。
これを実現するために、次のような関数を作成します。 ポイントクリックを追加、キャンバスの左上位置とクリック位置を見つけて、どのセルがクリックされたかを計算します。キャンバスに適用されているスケールを掛け合わせます。行と列を取得したら、 トグルセル 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.
終わり…まあ、そうでもないけど
これで、プラグ可能なウィジェットに関するこのシリーズは終了です。シンプルなカウンターウィジェットの作成から、WebAssemblyを Mendix アプリ。
これで終わりというわけではありません。今後数週間から数か月にわたって、さらに素晴らしいウィジェット コンテンツが提供される予定です。このシリーズについて、何かご要望やご意見がありましたら、このストーリーにコメントを残してください。