ゲームの乱数が毎回同じになる:シードと初期化のミス
遊びをつくる執筆にClaudeを使用しています。

ゲームを作っていて「敵の動きが毎回同じ」「アイテムの出現パターンが変わらない」と気づいたとき、真っ先に疑うべきなのは乱数のシード(seed)です。シードとは乱数列を決める出発点の数値で、同じシードを与えると必ず同じ順序の乱数が生成されます。この仕組みを知らずにコードを書くと、見た目は動いているのに毎回同一の展開になるゲームが完成してしまいます。 問題の根本はシンプルです。多くの言語や環境で乱数は「疑似乱数(pseudo-random number)」です。数学的な計算で乱数らしい数列を作るため、計算の出発点が同じなら結果も同じになります。これ自体は設計上の欠陥ではなく、デバッグや再現テストに意図的に使う場合もあります。ゲーム本番では「毎回違う結果」が必要なので、起動のたびに異なるシードを与える初期化が不可欠です。 この記事では、乱数が同じになる原因を構造から整理し、JavaScript・Python・GDScriptそれぞれで起きやすいミスと修正手順を示します。
この記事の目次
なぜ同じシードが使われ続けるのか
疑似乱数生成器(PRNG)は内部に「状態」を持ちます。random() や Math.random() を呼ぶたびに状態が進み、次の数値が返ります。プログラム起動時にこの状態をどう設定するかが「初期化」です。
初期化をしなかった場合、言語やライブラリはデフォルトのシードを使います。デフォルトが固定値(例:0 や 12345)なら毎回同じ列になります。一方、PythonのrandomモジュールやJavaScriptのMath.random()は実行環境がシステム時刻やOSのエントロピーを使って自動で初期化するため、通常は問題が起きません。
「それなら問題は起きないはずでは」と思うかもしれませんが、実際のゲーム開発では次の三つのパターンで同じ列が固定されます。
パターン1:自前のPRNGに固定シードをハードコードしたゲームのリプレイ機能や手続き生成マップを作るためにPRNGを自作し、テスト中に固定値のシードを書いたまま本番コードに残すケースです。最も多いミスです。
パターン2:シード初期化を条件分岐の外に書いた
if (newGame) {
rng.seed(Date.now()); // ← ここに書いたつもりが
}
rng.seed(0); // ← 直後にリセットされているこのように初期化を二重に書いてしまうと、後の行で固定シードに上書きされます。
パターン3:外部ライブラリのデフォルト動作を誤解したGDScript(Godot)のrandomize()を呼ぶ前にrandi()を使った、あるいはC#のRandom()をnew Random(0)で生成したまま再利用しているケースです。
JavaScriptで起きやすいミスと修正
Math.random() 自体は環境が初期化するため通常は問題ありません。問題が起きるのは、独自のPRNGアルゴリズム(Mulberry32やxoshiro128など)をゲームに組み込んだときです。
典型的なミスコード:
function mulberry32(seed) {
return function() {
seed |= 0; seed = seed + 0x6D2B79F5 | 0;
// ...(以降の計算)
}
}
const rng = mulberry32(42); // ← 42が固定シードに 42 など定数を書くとリロードのたびに同じ列です。
修正:
const rng = mulberry32(Date.now() ^ Math.random() * 0xFFFFFFFF | 0);Date.now() はミリ秒単位のタイムスタンプです。Math.random() と組み合わせることで、同一ミリ秒に複数インスタンスを作っても重複を避けやすくなります。シードをローカルストレージやURLパラメータから読む設計のゲームでは、「デバッグ用固定シード」を切り替えるフラグを別途用意し、本番ビルド時は必ず外す習慣をつけましょう。
PythonとGDScriptで起きやすいミスと修正
Python(Pygameなど):
Pythonのrandomモジュールはインポート時に自動初期化されます。ただしrandom.seed()を明示的に呼ぶと上書きされます。デバッグのために random.seed(0) と書いたコードを消し忘れるのが典型的なミスです。リリース前にコードベースを検索して random.seed( が残っていないか確認してください。
また、numpy.randomを使う場合はモジュールが独立した状態を持ちます。random.seed() を呼んでも numpy.random には影響しません。両方使うなら両方を初期化する必要があります。
GDScript(Godot Engine):
Godotにはグローバルなrandomize()とノードのRNG(RandomNumberGeneratorクラス)の二系統があります。
# グローバル系
randomize() # ← これを_ready()で呼ばないと固定シードのまま
var n = randi()
# クラス系
var rng = RandomNumberGenerator.new()
rng.randomize() # ← インスタンスごとに必要RandomNumberGeneratorをnew()しただけではseedが0です。rng.randomize()を呼ぶのを忘れると常に同じ列が出ます。複数のノードがそれぞれRNGを持つ設計では、全インスタンスが同タイミングでrandomize()を呼ぶと時刻が近く似た列になる場合があります。その場合はシードにrng.seed = randi()のように他のRNGの値を使う方法が有効です。
意図的な固定シードとの使い分け
固定シードが正解の場面もあります。判断基準として次を参考にしてください。
- 毎回異なる展開にしたい、シードの扱い:起動時にランダム初期化
- リプレイ・シード共有機能、シードの扱い:シードを保存・表示し、入力可能にする
- バグ再現・自動テスト、シードの扱い:固定シードをテストコードだけで使い、本番コードに残さない
- 手続き生成マップの保存、シードの扱い:マップIDとしてシードを記録し、ゲーム本体の乱数とは分離する
手続き生成マップとゲームプレイ乱数を同じRNGインスタンスで管理すると、マップ生成で消費した乱数の数がプレイ中の乱数に影響します。「マップ用RNG」「プレイ用RNG」と分けて持つのが安全です。
修正後の確認手順
- ゲームを終了し、再度起動して最初の乱数的な挙動(敵の初期配置、アイテムドロップなど)を記録する。
- もう一度同じ操作をして結果が異なることを確認する。
- 3〜5回繰り返して毎回異なることを確かめる。
「1回だけ違った」は偶然の一致の可能性があるため、複数回の確認が必要です。特にシードが時刻由来の場合、テストを素早く繰り返すと近い値のシードになりやすいため、数秒間隔を空けて確認するのが確実です。
また、修正後に「デバッグ用固定シード」がコードに残っていないか、テキスト検索で .seed(0) .seed(1) randomize(0) などを確認する習慣をつけると、リリース直前の見落としを防げます。
次の行動
まずゲームのコードを開き、seed randomize random.seed のいずれかで検索して定数が渡されていないかを確認してください。見つかった場合は、用途(本番か・テストかのみか)を判断し、本番コードから除去するか時刻ベースの初期化に切り替えましょう。修正したゲームはアソバースに投稿して実際のプレイ環境で動作を確かめると、ローカルでは気づかない問題を見つけやすくなります。
参考資料
- MDN Web Docs「Math.random()」
- Python公式ドキュメント「random — 擬似乱数生成器」
- Godot Engine公式ドキュメント「RandomNumberGenerator」

