godot-rustを(一部)使ってライフゲームを作ってみる
この記事について
前回、GodotEngineの基本的なノードとGDScriptを使って30x30の簡素なライフゲームを作ってみました。
今回は200x200=40000個のセルでもそこそこ動くように作ってみます。
完成品はこちらドン!
gifのフレームレート的なのがよくわかんないですが、実際はもうちょっと遅い。
2022/01/12追記
実際に動くやつ https://sayamapp.github.io/godot_game_of_life_wasm/
ソースコードとか https://github.com/sayamapp/game_of_life_wasm
godot-rustについて
素晴らしい記事がありますです。
素晴らしい記事がありますです。これでrust側からGodotをアレコレするやり方はもう完璧。
私もちょっとだけ作ってみたりしたことがありました。
そして挫折。rustだけで書くの超大変。さっきコード見たら何書いてるのかさっぱりわからんし。
でもちょっとだけなら使いやすいかも。
ということで、ここではGDScriptからrustの構造体をGodotのクラスとして使うやり方を書きたいと思います。
こういう事がやりたいです。
やりかた
.gdnsだの.tresだの、謎のファイルの使い方について知る必要がありました。
結論から言うとこの2つのファイルはただのリソースで、たいして面白いことも書いてありませんでした。
tresファイルにはライブラリの場所、gdnsファイルには使いたいクラスの名前とtresファイルの場所が書いてあればいいみたいです。
ということでこんな感じでソースを書きました。
[lib]
crate-type = ["cdylib"]
[dependencies]
gdnative = "0.9.1"
rand = "0.6.5"
乱数を使うのでcargo.tomlにrandクレートを追加。
use gdnative::prelude::*;
use rand::Rng;
// Nodeを継承したGameOfLifeという名前の構造体をGodotのNativeClassとして使いたい!
#[derive(NativeClass)]
#[inherit(Node)]
struct GameOfLife {
cells: Vec<Vec<bool>>,
}
#[methods]
impl GameOfLife {
// これ呼ぶと使えるようになるよ
fn new(_owner: &Node) -> Self {
GameOfLife { cells: Vec::new() }
}
// new()には引数を渡せないみたいなので実際の初期化はこっちでやるよ
#[export]
fn init(&mut self, _owner: &Node, size: usize) {
for _ in 0..size {
let mut _a = Vec::new();
for _ in 0..size {
_a.push(rand::thread_rng().gen::<bool>());
}
self.cells.push(_a.clone());
}
}
// ライフゲームの計算はGDScriptだとちょっと重いのでこっちでやるよ
// GDScriptの_process(_delta)とかから毎フレーム呼んで計算させるよ。
#[export]
fn calc(&mut self, _owner: &Node) {
let len = self.cells.len();
let mut next = vec![vec![false; len]; len];
for y in 0..len {
for x in 0..len {
let mut lives = 0;
for dy in -1..2 {
for dx in -1..2 {
if dy == 0 && dx == 0 {
continue;
}
let pos_x = (dx + (x + len) as isize) as usize % len;
let pos_y = (dy + (y + len) as isize) as usize % len;
if self.cells[pos_y][pos_x] {
lives += 1;
}
}
}
next[y][x] = lives == 3 || (self.cells[y][x] && lives == 2);
}
}
self.cells = next.clone();
}
// GOScriptから座標を貰ったらその座標のセルの生死をboolで返すよ
#[export]
fn get_status(&self, _owner: &Node, x: usize, y: usize) -> bool {
self.cells[y][x]
}
}
// ここらへんのおまじないでいい感じにしてくれるよ。
fn init(handle: InitHandle) {
handle.add_class::<GameOfLife>();
}
godot_init!(init);
あとはメインループとかをGDScriptで書きました。
extends Node
var cells = {}
const FIELD_SIZE = 200
const TEXTURE_SIZE = 3.2
# セルのプレハブ的なシーン
onready var cell_scene = preload("res://Cell.tscn")
# preload(gdns).new() でgdnsファイルで指定した構造体のインスタンスが使える。
onready var game_of_life = preload("res://game_of_life.gdns").new()
func _ready():
# セルをガガガッとインスタンスにして辞書にコレクション
for y in FIELD_SIZE:
for x in FIELD_SIZE:
var _cell = cell_scene.instance()
add_child(_cell)
_cell.position = Vector2(x, y) * TEXTURE_SIZE
_cell.visible = false
cells[Vector2(x, y)] = _cell
# GameOfLife構造体メンバの2次元配列を200x200で初期化
game_of_life.init(200)
# 結果をもとにセルの表示を変えるdraw_field()を呼ぶ。
draw_field()
# 毎フレームライフゲームの計算をさせ、draw_field()する。
func _process(_delta):
game_of_life.calc()
draw_field()
# セルの生死を問い合わせてセルの表示、非表示を変える
func draw_field():
for key in cells:
cells[key].visible = game_of_life.get_status(int(key.x), int(key.y))
gdnsをpreloadしてnew()するだけでrustで作った構造体が使えます。
rustだと構造体だけどGodotだとクラスなので若干何を書いているのか分かりづらい。
完成
GDScriptだけで書いたときと比べて、
330(ms/frame) -> 220 (ms/frame) の改善ができました。
感想とか
速くて最高!godot-rust!これしかない!って感じでは無いです。たとえばrustの側でVecの代わりにHashMapとか使うとガクッと遅くなったりしますし、ライフゲームのアルゴリズム自体を改善したほうが良くね?という感じもあるし、そもそも面倒くせぇのであんまり過信したり、まして最初から使う気まんまんで設計するのもちょっと違うかなーというのが正直な所。
Godotは最初からかっちりと設計を考えるより、GDScriptで治安の悪いコードを書いたり消したりしながら作るほうが楽しいし自分には向いてる気がしました。極端な話Godotでプロトタイピングして、面白いゲームの気配がしたら本番はUnityで作るとかそういうやり方も全然アリだと思う(私はUnity挫折しましたけど)。ブレワイも最初は2Dから作ったみたいな話もありますし。ということでGDScriptがオススメ。でもrustも楽しいから困る。ゲーム作るのは楽しいけど楽しいゲームを作るのは大変。自分で作るとスーパーモンキー大作戦が神ゲーに思えるから不思議です。
あと、こういう記事を書く時の教科書みたいのがあったら教えてほしいです。読みづらくて申し訳ない。
Discussion