世界を動かす技術を、日本語で。

Goでゲームを作る:LLMなしの3ヶ月とLLMありの3日

概要

  • 15年のソフトウェアエンジニア経験 を活かし、初めてカードゲームを公開
  • GoとReact、WASM を使ったサーバーレス構成で実装
  • LLM(大規模言語モデル)前後の開発速度の違い を体験
  • Tic-Tac-Toeリポジトリ を使ったゲーム開発の最小手順を解説
  • WASMとの連携やトラブルシューティング まで具体的に紹介

15年目の初ゲーム公開体験

  • 15年のソフトウェアエンジニア歴 を持つ筆者が、初めてゲーム開発と公開に挑戦

  • アルゼンチンの伝統的カードゲーム「Truco」 を題材に選定

  • Goでバックエンド を構築、 ReactでUI を最小限実装

  • サーバー不要 を目指し、 TinyGoでWASM化 しGitHub Pagesでホスティング

  • LLM普及前 の時代、全て手探りで3ヶ月かけて完成

  • 広告や収益化は一切せず、純粋に完成を目指した作品

  • 公開から1年経過後も プレイヤーが継続的に遊んでいる実績

    • Truco(ゲーム本体)
    • バックエンド(Go)
    • フロントエンド(React、1時間の学習で実装)

LLM時代の開発「Escoba」:3日で実装

  • 翌年、 家族にEscobaを教えるため に新ゲーム開発を決意
  • LLM(Claude)を活用 し、TrucoのバックエンドをクローンしてEscoba用にリファクタリング
  • 初回プロンプトでほぼ完璧に動作 し、バグ修正も最小限
  • フロントエンドは自力で数日かかったが、 Reactスキルの課題WASMによる状態管理の複雑さ が主な要因
  • デバッグもJavaScriptで苦労
  • Escoba(ゲーム本体)、バックエンド(Go)、フロントエンド(React)を公開

最小構成で自作ゲームを作る手順

バックエンド(Go)の基本構成

  • GameState構造体 で状態管理(初期盤面やアクション履歴など)
  • CalculatePossibleActions で有効な手を算出
  • RunAction でGameStateを更新
  • Bot用関数 で自動プレイにも対応
  • ヒューマン対ヒューマンは有料サーバー必須、基本はBot対戦推奨

フロントエンド(React)の基本構成

  • 新規GameState生成 のAPIコール
  • 盤面レンダリング
  • 有効な手から選択し、アクション適用
  • Botのターンを自動で処理
  • シンプルな構成で実装可能

バックエンドとWASM連携手順

WASMへのコンパイル

  • GOARCH=wasm GOOS=js go build でWASMバイナリ生成(ただし巨大化するためTinyGo推奨)
  • TinyGo用のmain.go を別途用意し、エクスポート関数を明示
  • main()の最後でselect {} を呼び、プログラムの即時終了を防止
//go:build tinygo
package main
func main() {
  js.Global().Set("trucoNew", js.FuncOf(trucoNew))
  js.Global().Set("trucoRunAction", js.FuncOf(trucoRunAction))
  js.Global().Set("trucoBotRunAction", js.FuncOf(trucoBotRunAction))
  select {}
}

データの受け渡し(JSON経由)

  • GameState等の構造体はJSONでやり取り
  • TinyGoのドキュメントを参考に、 js.CopyBytesToGo/ToJS でバイト列をやり取り
func trucoRunAction(this js.Value, p []js.Value) interface{} {
  jsonBytes := make([]byte, p[0].Length())
  js.CopyBytesToGo(jsonBytes, p[0])
  newBytes := _runAction(jsonBytes)
  buffer := js.Global().Get("Uint8Array").New(len(newBytes))
  js.CopyBytesToJS(buffer, newBytes)
  return buffer
}

フロントエンドからの呼び出し例

  • WASM関数呼び出し・GameState管理のグローバル変数化
function jsRunAction(data) {
  const encoder = new TextEncoder();
  const encodedData = encoder.encode(JSON.stringify(data));
  const result = trucoRunAction(encodedData);
  const json = new TextDecoder().decode(result);
  return JSON.parse(json);
}
let gameState = jsNewGame();
gameState = jsRunAction(action);
  • 状態管理はWASM側が唯一のソース となり、フロントエンド側で直接変更不可

バックエンド更新時のWASM再ビルド

  • Makefileで自動化推奨
  • wasm_exec.jsのコピーも必須
  • HTMLのHEADにscriptタグでwasm_exec.jsと初期化コードを追加
<script src="wasm/wasm_exec.js"></script>
<script>
  const go = new Go();
  const WASM_URL = 'wasm/wasm.wasm';
  var wasm;
  let wasmReady = false;
  if ('instantiateStreaming' in WebAssembly) {
    WebAssembly.instantiateStreaming(fetch(WASM_URL), go.importObject).then(function (obj) {
      wasm = obj.instance;
      go.run(wasm);
      wasmReady = true;
    })
  } else {
    fetch(WASM_URL).then(resp => resp.arrayBuffer() )
    .then(bytes => WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
      wasm = obj.instance;
      go.run(wasm);
      wasmReady = true;
    }))
  }
</script>

トラブルシューティング

  • WASMファイルが読み込めない場合
    • GitHub Pagesでは自動対応
    • ローカルでは http-server 等でHTTPサーバー起動
      • 例:npx http-server ./public -p 8080
      • ブラウザで http://localhost:8080 にアクセス

まとめ

  • ゲーム開発・公開の楽しさ と、 Go+React+WASMの実践的なノウハウ を共有
  • LLM活用による開発効率化 の実感
  • 自作ゲームに挑戦したい人向けの最小構成テンプレート と具体的手順
  • 質問や相談も歓迎、気軽に連絡可能

Hackerたちの意見

LLMは、前のゲームをテンプレートにしてコード面で3ヶ月の先行スタートを切ったし、もっと重要なのは、手動でのパスでの学びや失敗も活かせたことだね。

それだけじゃなくて、彼らにとって初めてのゲームだったから、未知の未知にたくさん直面してたんだよね。もし今日、LLMなしでカードゲームをプログラムし始めたとしても、前の経験から得た知識や洞察を活かせるから、3ヶ月もかからずにできると思う。

うん、これがクリックベイトだって分かってたけど、見た瞬間ちょっと驚いたよ:Trucoのバックエンドをクローンして、ClaudeにEscobaのルールを説明する長いプロンプトを与えて、コードをリファクタリングさせたんだ。人間の開発者が自分でコードをリファクタリングするのにどれくらいかかると思う?3日以上かかる可能性はあると思うけど、どうかな!

クローンする必要すらないよ。僕はゼロから数ヶ月かかるプロジェクトを再現したことがあるけど、次回は前回の1/3の時間で済む感じだね。

この投稿が好きなのは、多くの開発者が見落としがちなことを強調しているところ。ゲーム開発のコーディング部分は、実際にはボトルネックじゃなかったんだよね。ソロ開発者でも、AIがあろうがなかろうが、メカニクスをすぐに作り出せる。ほんとうに大変なのは、見えないレイヤーの部分。ループのバランス調整、難易度の調整、奇妙に見えないアセットの作成、そして誰かの注意を5分以上引きつけるための磨き上げ。だから、LLMの後に素晴らしいSteamのリリースが急に増えたわけじゃない。技術が一つの壁を低くしたけど、高い壁はまだ残ってる。2010年代のUnityの台頭みたいなもので、エンジンがゲーム制作を民主化したけど、良いゲームの爆発的な増加は見られなかった。ただの試みが増えただけ。LLMはコードに対して同じことをしていて、画像モデルはアートに対してそれを始めているけど、どちらもあなたのゲームが実際に楽しいかどうかは教えてくれない。私にとって興味深いのは、AIが実装だけでなくプレイテストもできるようになったらどうなるかってこと。ループの何千回もの反復を行い、シミュレーションされたプレイヤーを引きつけるメカニクスを浮き彫りにする。そうなった時、私たちは「AIを生産性向上のハック」として使うのを超えて、「デザインのコラボレーターとしてのAI」に進むことができる。まだそこには至っていないけど、この記事はその方向性における初期のデータポイントのように感じる。

私の生成AIのリトマス試験:2Dピクセルアートアクションゲームの完全なスプライトシートを生成してみて。例えば、バトルタンクやメインヒーローの動きだけでも。今のところ成功してないけど。

実際に面白いゲームプレイを持つゲームを作ることが、どの言語でゲームを作るのがベストかを議論するよりもずっと重要な理由だよね。

私たちは良いゲームの比例的な爆発を見なかった 確かに、2010年代の初め頃には良いインディーゲームの爆発があったよね。Unityに関係があったかどうかはまた別の話だけど。

私にとって興味深い質問は、AIが実装するだけでなく、プレイテストもできるようになったらどうなるかということです。ループの何千回もの反復を実行して、どのメカニクスがシミュレーションされたプレイヤーを引きつけるかを明らかにするのです。AIはどうやってプレイヤーをシミュレートするのでしょうか?そして、なぜAIが本物の人間が魅力的だと感じるものを判断できるべきなのでしょうか?

2010年代のUnityの台頭みたいなもんだね。エンジンがゲーム制作を民主化したけど、いいゲームの爆発的な増加は見られなかった。ただの試みが増えただけ。でも、実際には?限られたXBLAのカタログからはずいぶん進歩したよ。これは一晩で起こったわけじゃないけど、Unity、Godot、Gamemaker、Renpy、RPG Makerがなければ、今のゲームの量はなかっただろうね。

この投稿の好きなところは、多くの開発者が見落としがちなことを強調している点だ:ゲーム開発のコーディング部分は、実際にはボトルネックではなかった。ソロ開発者は、AIがあろうとなかろうと、メカニクスをかなり早く作り出せる。これは全然違うよ。ゲームに関わったことがない私には、「基本的な」ゲームを書くのにかなりの時間(数ヶ月かかることもある)を要する。ソフトウェア開発の良いプラクティスについてはたくさん知っているし、FAANGでの10年以上の経験もあるけど、ゲーム開発の細かい部分や基本すら知らない。最近、別の用途でこれを体験した。経験豊富なバックエンド開発者として、いくつかのJavaScript/ブラウザ関連の自動化をしたいと思った。2〜3日自分で試してみたけど、いくつかのプロトタイプはできたものの、実際には何も機能しなかった。AIと2時間過ごしたら、動作するソリューションができたんだ。一緒に素早く反復して、いくつかのランタイムの問題も解決できて、今はスムーズに動いてる。だから、私のような経験豊富な開発者にとっても、AIの価値は確実にあると思う。

AIが実装だけじゃなくて、プレイテストもできるようになったらどうなるの? 何千回もループを回すことができるんだ。アタリのゲームからスタークラフトまで、LLMが登場する前からそういうことはあったよね。 > シミュレーションされたプレイヤーを引きつけるメカニクスを浮き彫りにするのは、どうやって実現するかは不明だね。いろいろあるけど、全てのゲームが全ての人に合うわけじゃないし。

Hacker Newsで議論の続きを見る