【Godot】敵の移動アルゴリズム

今回はGodot Engineで敵の移動アルゴリズムを作る方法を解説します。

追跡

まずはプレイヤーの方向に愚直に向かってくる方法です。 このパターンは近距離攻撃と得意とする敵が使います。

この実装をするのは簡単で、プレイヤーの座標がわかればそこに向かっていくだけです。

const MOVE_SPEED = 100.0

func _process(delta:float) -> void:
  var player:Player # プレイヤー.
  var v := player.position - self.position # プレイヤーへの方向ベクトルを計算.
  v = v.normalize() # 正規化.
  v *= MOVE_SPEED # 移動速度.
  self.position += v * delta

ただこの動きは単調なので「一時停止」「旋回速度を考慮する」といった変化をつけると良いです。

var _timer := 0.0 # 経過時間.
var _wait_time := 0.0 # 一時停止時間.

# 移動処理.
func _update_move(delta:float) -> void:
    if _wait_time > 0:
        # 一時停止中.
        _wait_time -= delta
        return
    
    _timer += delta
    if _timer > 3.0:
        # 3秒動いたら1秒止まる.
        _wait_time = 1.0
        _timer -= 3.0
        return
    
    # 移動処理.
    var v = Vector2.LEFT
    ...

完全に停止するだけでなく、速度に緩急をつけてみても良いと思います。

「旋回」は移動する方向を角度として保持し、旋回速度に制限を持たせる方法です。

旋回速度の上限を持たせると、プレイヤーは後ろに回り込むことで安全に敵を倒せるという攻略が生まれます。

# 狙い撃ち角度を取得.
var aim = Common.get_aim(self)
# 現在の角度に対する角度差を求める.
var d = Common.diff_angle(_angle, aim)
if abs(d) > 90:
    _speed = MOVE_SPEED * 0.3 # 速度制限.
else:
    _speed = MOVE_SPEED
if abs(d) > 30: # 旋回速度制限 (30度想定).
    d = sign(d) * 30
aim = _angle + (d * delta)

diff_angle() は角度差を求めるもので、作っておくと旋回処理を行う際にとても便利です。

extends Node

class_name Common

# プレイヤー.
static var _player:Player

# セットアップ.
static func setup(player:Player) -> void:
    _player = player

# 狙う撃ち角度を求める.
static func get_aim(node:Node2D) -> float:
    var angle := _player.position.angle_to_point(node.position)
    return rad_to_deg(angle * -1) # 上を90度にしたいので反転.

# 最短の角度差を求める
# @param now 現在の角度
# @param next 目標となる角度
static func diff_angle(now:float, next:float) -> float:
    var d := next - now
    d -= floor(d / 360.0) * 360.0
    if d > 180:
        d -= 360
    return d

2025.9.22 追記:lerp_angle()で角度の補間が可能

Godotの標準関数をあまり調べていなかったのですが lerp_angle() を使うと2つの角度から最短の角度に向かって補間した角度を求めることができます。

以下、実装例です。

extends Node2D

const ROTATE_SPEED = 0.5 # 旋回速度.

var _obj_pos := Vector2(480, 320) # 初期位置.
var _obj_angle := PI/2 # 最初は下向き.

func _process(_delta: float) -> void:
    # マウスカーソルに向かって動く.
    var target = get_global_mouse_position()
    # 狙う撃ち角度.
    var aim = _obj_pos.angle_to_point(target)
    _obj_angle = lerp_angle(_obj_angle, aim, ROTATE_SPEED * _delta)
    
    # 描画.
    queue_redraw()

func _draw() -> void:
    # オブジェクトの位置を描画.
    var size := Vector2(32, 32)
    var rect := Rect2(_obj_pos-(size/2), size)
    draw_rect(rect, Color.FOREST_GREEN)
    # 視線を描画.
    var lay = Vector2(1280, 0).rotated(_obj_angle)
    draw_line(_obj_pos, _obj_pos+lay, Color.RED)

ただ私が自作した diff_angle() と比較すると、「1. 補間率に合わせた回転後の角度を返す(角度差ではない)」「2. 角度の単位は "ラジアン" を入出力とする」といった相違点があります。そのため、場合によっては使い方に工夫が必要となるかもしれません。

逃走

プレイヤーとは逆の方向に移動するパターンです。

これは「追跡」のベクトルを逆、または180度反転させます。

# 狙い撃ち (プレイヤーへの) 角度.
var aim = Common.get_aim(self)
# 180度回転すると逆向きになるので離れる
aim += 180

間合い (距離) を取る

プレイヤーから離れすぎず、近づきもしないパターンです。 このパターンは中〜遠距離から攻撃する敵の行動として使います。

例えば以下の行動ルールです。

  • 「300」より近づくと「逃走」
  • 「500」より離れると「追跡」
  • それ以外 (300〜500)の場合は立ち止まる

このルールを実装すると以下のコードとなります。

# 狙い撃ち角度の取得.
var aim = Common.get_aim(self)
# プレイヤーとの距離を計算.
var dist = Common.get_target_distance(self)
# 速度を設定.
_speed = MOVE_SPEED
if dist < 300:
    aim += 180 # 近づいたら離れる
elif dist < 500:
    _speed = 0 # 300〜500の場合は様子見 (立ち止まる).
else:
    pass # 500以上離れていたら近づく.

回り込む

間合いを取るに近いですが、周りを取り囲む(周りを回転する) ように移動するパターン。

例えば以下の行動ルールです。

  • 「200」より近づくと「逃走」
  • 「300」より離れると「追跡」
  • それ以外 (200〜300)の場合は回り込む

このルールを実装すると以下のコードとなります。

var aim = Common.get_aim(self)
var dist = Common.get_target_distance(self)
_speed = MOVE_SPEED
if dist < 200:
    aim += 180 # 近づいたら離れる
elif dist < 300:
    aim += 90 # 回転する.
else:
    pass # 500以上離れていたら近づく.

関連記事

2dgames.jp

【Godot】弾幕2DSTGの基本

今回はGodot Engineで弾幕2DSTGを作るときに知っておきたい基本をまとめておきます。

数学

ベクトル

これは2Dゲームを作るときに共通することですが、座標や速度などはベクトルを使います。 ベクトルとは「向きと大きさの2つを同時に持つ量」のことで、物体の位置や移動を表す際に用いられます。

上記の2Dベクトルであれば、(X, Y) = (6, -5) というベクトルです。2Dゲームにおけるベクトルは数学的なベクトルと異なり、上が "-Y" となるのが特徴です。

なおGodot Engineには、2Dベクトルを扱うためのクラスが用意されています。2Dベクトルを扱うときに知っておくと便利な関数は以下のページにまとめています。

2dgames.jp

極座標

弾幕を扱うときに便利なのが極座標系です。ベクトルのような直交座標系は X/Yの要素を持ちますが、極座標系は r (距離) と θ (角度) の要素となります。

例えば (X, Y) = (4, -4) の方向であれば、θは atan2(-4, 4) = -0.785...≒45度、rは √(42 + (-4)2) = 5.656.... となります。

具体的には、角度と速度を指定して敵弾を発射できると、3way弾のパターンで弾の広がり具合(角度)と弾の発射速度を直感的に変更しやすくなります。

このように中心を基準に左右の広がり角度と弾の速度を決めると、3way弾が作れます。

Godot の Vector2での角度の扱い

2Dゲーム (スクリーン座標系) では、上を -Y、下を+Yとして扱いますが、数学的には上が+Yなので座標軸の扱いに差異があります。 そして Godot の Vector2 などには Vector2.angle() などベクトルを角度 (ラジアン) に変換する関数が用意されていますが、これらは 上を+Yとして扱っているため、スクリーン座標系と逆の扱いとなります。

このことについて、どのように扱うかは人によります。

  1. 数学的な正しさを優先して Godotのangle()をそのまま使う
  2. 扱いやすさを考えて、Y軸をマイナスで反転させる

1は余計な変換をせずに扱えますが、弾幕のパターンを作るときにY軸を逆にした角度を与える必要があります。 2はベクトルと角度を変換する際に変換処理を書く必要がありますが、直感的に角度を扱えるメリットがあります。

この記事では「2. 扱いやすさを考えて、Y軸をマイナスで反転させる」という実装方法で進めていきます。

狙い撃ち弾

2DSTGの基本パターンとして狙い撃ち弾があります。 これは文字通り、敵からプレイヤーに向けて発射する弾です。

(※自機画像は 有限会社エムツー様よりお借りしました)

ベクトルのみで処理するのであれば、敵の位置を A、プレイヤーの位置をBとした場合、ベクトルの差 (B-A) を求めて正規化した後、適切な速度をかけることで移動ベクトルが求められます。

ただ弾幕ゲームでは、狙い撃ち弾を中心に3way弾を作りたいことが多いので「狙い撃ち角度」を計算したほうが都合が良いです。 計算式はベクトルの差(B-A)=Vを求めて、atan2(v.y, v.x) で角度(ラジアン)が求められます。

ただ Godot Engineの場合は、Vector2に angle_to_point() という指定の位置に向けた角度を簡単に取得できるのでこれを使うのが便利です。

# 狙い撃ちの角度.
static func get_aim(pos:Vector2) -> float:
    # プレイヤーの位置.
    var target = _player.position
    # 狙い撃ち角度 (ラジアン)の取得.
    var rad = pos.angle_to_point(target)
    # ラジアンからディグリーに変換.
    return rad_to_deg(-rad) # 逆回転にしたいので -1

補足として、この解説ではスクリーン座標系で角度を扱いたいのでベクトルと角度の変換が入るところで "-1" をかけ合わせ逆回転しています。

なお敵が狙い撃ちするためにはプレイヤーの座標が必要なので、static変数にプレイヤーのインスタンスを保持しておくのがおすすめです。

extends Node

class_name Common

# プレイヤー.
static var _player:Player
# 敵弾.
static var _bullets:CanvasLayer

# セットアップ (インスタンスを保持しておく).
static func setup(player:Player, bullets:CanvasLayer) -> void:
    _player = player
    _bullets = bullets

ラジアン (Radian) とディグリー (Degree)

角度を扱う上で知っておきたいのが、ラジアンとディグリーの違いです。 Godotを始め、数学ライブラリでは角度はラジアン単位で扱われます。

ただ弾幕パターンを作るときは度数法のディグリーを使うと扱いやすいです。

ラジアンとディグリーの相互変換は以下の計算式となります。

ただGodotには、専用の変換関数が用意されているのでこれらを使うのがおすすめです。

  • rad_to_deg(): Radian → Degree
  • deg_to_rad(): Degree → Radian

N-way弾

扇状に広がる 3way弾や4way弾などの作り方です。

関数のパラメータとしては、以下のパラメータを渡せると良いです。

  • 発射数: 発射する弾の数。多くなるほど弾の密度が上がる
  • 中心角度: 扇状に広がる弾の中心角度
  • 範囲: 扇の広さを表す角度。大きくなるほど密度が小さくなります
  • 速度: 弾の速度

Godotでの実装例は以下のとおりです。

## N-wayを撃つ
## @param n 発射数.
## @param center 中心角度.
## @param wide 範囲.
## @param speed 速度.
func _nway(n, center, wide, speed) -> void:
    if n < 1:
        return
    
    var d = wide / n # 弾の間隔
    var a = center - (d * 0.5 * (n - 1)) # 開始角度
    for i in range(n):
        _bullet(a, speed)
        a += d

N-way弾の特徴として、発射数が "奇数" である「奇数弾」、"偶数" である「偶数弾」によって扇の中心の扱いが変化します。これによって「奇数弾」と「偶数弾」を交互に繰り返すことに、弾をジグザグにすり抜ける遊びが作れます。

遅延発射

2DSTGを作るときに是非とも実装しておきたいのが「遅延発射」です。

例えば「1秒後に 220度の方向に10の速さで発射する」という情報を配列などに保持しておき、1秒経過後に弾を発射する仕組みです。 これを作っておくと、上記の画像のような Whip弾 (しなる弾幕) が実装しやすくなります。

Godotでのサンプルコードは以下のとおりです。

class DelayedBatteryInfo:
    """ 遅延発射弾の情報 """
    var deg:float = 0 # 角度.
    var speed:float = 0 # 速さ.
    var delay:float = 0 # 遅延時間(秒).
    var ax:float = 0 # 加速度(X)
    var ay:float = 0 # 加速度(Y)
    func _init(_deg:float, _speed:float, _delay:float, _ax:float=0.0, _ay:float=0.0) -> void:
        """ コンストラクタ """
        deg = _deg
        speed = _speed
        delay = _delay
        ax = _ax
        ay = _ay
    func elapse(delta:float) -> bool:
        """ 時間経過 trueで発射可能. """
        delay -= delta
        if delay <= 0:
            return true # 発射できる.
        return false

# 弾のディレイ発射用配列.
var _batteries: Array[DelayedBatteryInfo] = []

## 弾を撃つ.
## @param deg 角度
## @param speed 速さ
## @param delay 発射遅延 (秒)
## @param ax 加速度(X)
## @param ay 加速度(Y)
func _bullet(deg:float, speed:float, delay:float=0, ax:float=0, ay:float=0):
    if delay > 0.0:
        # 遅延発射なのでリストに追加するだけ.
        _add_battery(deg, speed, delay, ax, ay)
        return null
    
    # 発射する.
    var b = BULLET_OBJ.instantiate()
    b.position = position
    b.set_velocity(deg, speed)
    b.set_accel(ax, ay)
    var bullets = Common.get_layer("bullet")
    bullets.add_child(b)
    return b

## 遅延発射リストに追加する.
func _add_battery(deg:float, speed:float, delay:float, ax:float, ay:float) -> void:
    var b = DelayedBatteryInfo.new(deg, speed, delay, ax, ay)
    _batteries.append(b)

## 更新時間を固定化するため "_physics_process" で更新
func _physics_process(delta: float) -> void:
    _timer += delta
    # 遅延発射更新.
    _update_batteies(delta)

## 遅延発射リストを更新する.
func _update_batteies(delta:float) -> void:
    var tmp:Array[DelayedBatteryInfo] = []
    for battery in _batteries:
        var b:DelayedBatteryInfo = battery
        if b.elapse(delta):
            # 発射する.
            _bullet(b.deg, b.speed, 0, b.ax, b.ay)
            continue
        
        # 発射できないのでリストに追加.
        tmp.append(b)
    
    # 発射できない弾は次回に持ち越し.
    _batteries = tmp

ポイントとしては、更新関数を _physics_process() にして更新時間を固定にしているところです。通常の _process() の更新頻度には偏りがあるためこれを使うと弾幕の発射タイミングにムラが発生します。ですが、_physics_process() を使うことで発射タイミングがズレにくくなります。

遅延発射にコルーチンやスレッドを使う方法もありますが、個人的に非同期処理をあまり入れたくないのでこのようにしています。ただ処理をマルチスレッドで最適化したい場合や、非同期処理に抵抗がない場合は、コルーチンやスレッドにしても良いと思います。

なお発射遅延を実装することで、先程のN-way弾にWhip弾を組み合わせられます。

## N-Wayを撃つ
## @param n 発射数.
## @param center 中心角度.
## @param wide 範囲.
## @param speed 速度.
## @param delay 発射遅延時間 (秒).
func _nway(n:int, center:float, wide:float, speed:float, delay:float=0.0) -> void:
    if n < 1:
        return
    
    var d = wide / n # 弾の間隔
    var a = center - (d * 0.5 * (n - 1)) # 開始角度
    for i in range(n):
        _bullet(a, speed, delay)
        a += d

針弾

針弾とは、針のように長細い弾のことです。

2DSTGをあまりやらない人には "針弾" は「単に見た目が長細いだけの弾では?」という印象を受けますが、実は弾幕STGを作る上で重要な効果を持ちます。

効果 説明
1. 進行方向の可視化 長細いことで、弾の進む方向 (角度) がわかりやすい
2.塊としての認識 通常はつながって発射されることで、弾の塊を認識しやすい
3. 当たり判定の分かりやすさ ・見た目の形と実際の判定がほぼ一致しやすいため、プレイヤーが「避けやすい」と感じる
・丸弾に比べて「横幅が狭い」ため、判定が小さく感じられ、スレスレを抜ける緊張感を演出できる
4. 弾速との相性 ・針弾は「速い弾」に使われやすい: → 長細さが速度感を強調し、プレイヤーに迫ってくる怖さや緊張感を与える
・同じ速度の丸弾よりも「速く見える」心理効果がある

他にも針弾を連ねることで「レーザーの簡易版」のように壁を作るので、弾幕パターンの区切りとなる効果などが考えられます。

具体的な発射方法の実装例としては、"発射角度" と "速度" を固定して先程の「発射遅延」で連続発射すると実装できます。

for i in range(5):
    var aim = _aim() # 狙い撃ち.
    var delay = (0.05 * i) # ディレイ時間.
    _bullet(aim, 500, delay) # 発射.

画像は丸弾ですが、このように連結させることで針弾のように見せられます。

全方位弾

全方向に弾を発射する全方位弾は 360度の均等な間隔で弾を生成すると実装できます。

for i in range(36):
  _bullet(i * 10, 100) # 10x36=360度.

回転弾

全方位弾に遅延発射を適用すると回転弾となります。

これは回転弾に限りませんが、高速弾を発射する場合は、あえて後ろから発射を開始することで予兆の演出としても使えます。

var aim = _aim()
var deg1 = aim - 155 # 斜め後ろから開始
var deg2 = aim + 155 # 斜め後ろから開始
for i in range(36):
  var speed = 100 + 20 * i
  var delay = 0.05 * i
  _bullet(deg1, speed, delay)
  _bullet(deg2, speed, delay)
  deg1 += i * 0.5
  deg2 -= i * 0.5

重力 (加速) 弾

下方向に加速する弾です。

等速移動でないため、やや回避に慣れのいる弾です。 弾の発射に加速度を持たせることで実装できます。

# 敵弾クラス.
class_name Bullet

var _velocity:Vector2
var _acceleration:Vector2

func _process(delta: float) -> void:
    # 加速度を加算.
    _velocity += _acceleration * delta
    # 速度を加算
    position += _velocity * delta

なおY軸だけでなく、X軸方向に加速させるとウェーブ弾になります。

Godotサンプル

Godotで弾幕パターンを実装したサンプルをHTML5として出力しました。以下のページから確認できます。 syun777.sakura.ne.jp

ここのドロップダウンリストから弾幕パターンが選べます。

またソースコードGithubにアップロードしています。

github.com

読みやすいゲームプログラムを書くための 7つの基本テクニック

この記事では、可読性が高く機能拡張しやすい(保守しやすい)ゲームプログラムを書くための 7つの基本的なテクニックを紹介します。

読みやすいゲームプログラムコードを書くための7つの基本テクニック

今回紹介する方法は以下のものです。

  • 1. ローカル変数を使う
  • 2. 長い条件式を関数にする
  • 3. 長いswitch-case文を関数化する
  • 4. テーブルで処理をシンプルにする
  • 5. 配列へのアクセスは関数を経由する
  • 6. インデントは深くしない
  • 7. スコープの大きさに適した名前をつける

1. ローカル変数を使う

以下はマウスでクリックした場所に対して、プレイヤーを移動させるようなコードです。

    // 1マスあたりのマップサイズ.
    const int MAP_SIZE = 32;
    // マップデータ.
    int map[12][12] = {};
    // プレイヤーの座標.
    Vec2 playerPos = {};

    // マウスカーソルの座標を取得する.
    Vec2 mouse = getMousePos();
    if(map[mouse.x/MAP_SIZE][mouse.y/MAP_SIZE] == 0) {
        // マウスカーソルの位置に対応するマップの位置に
        // 何もなければプレイヤーをそこまで移動させる.
        playerPos.x = mouse.x/MAP_SIZE;
        playerPos.y = mouse.y/MAP_SIZE;
        // プレイヤーがいるフラグを立てておく.
        map[mouse.x/MAP_SIZE][mouse.y/MAP_SIZE] = 1;
    }

気になる部分は、マウス座標に対応するマップの位置の計算を複数記述している部分です。(「mouse.x/MAP_SIZE」「mouse.y/MAP_SIZE」という計算が複数書かれています)

これをローカル変数として定義することでコードの圧迫感を減らすことができます。

    // 1マスあたりのマップサイズ.
    const int MAP_SIZE = 32;
    // マップデータ.
    int map[12][12] = {};
    // プレイヤーの座標.
    Vec2 playerPos = {};

    // マウスカーソルの座標を取得する.
    Vec2 mouse = getMousePos();
    // 計算結果をローカル変数に入れておく.
    int mx = mouse.x/MAP_SIZE;
    int my = mouse.y/MAP_SIZE;
    if(map[mx][my] == 0) {
        // マウスカーソルの位置に対応するマップの位置に
        // 何もなければプレイヤーをそこまで移動させる.
        playerPos.x = mx;
        playerPos.y = my;
        // プレイヤーがいるフラグを立てておく.
        map[mx][my] = 1;
    }

コードの行数は増えますが、コードの横幅を少し減らすことができます。

📌計算式をローカル変数に置き換えるメリット
  1. コードの横幅が減って圧迫感がなくなる
  2. 計算式に修正があったときに変更箇所が1箇所で済む

2. 長い条件式を関数化する

以下はオブジェクトが画面内に存在するかどうかを判定するコードです。

    // 画面サイズ.
    Size screen = getScreenSize();
    // 現在座標.
    Vec2 pos = getPlayerPos();
    // オブジェクトの大きさ.
    float size = 16.f;

    // 画面外に出たかどうか.
    if(pos.x < -size
            || pos.y < -size
            || pos.x > screen.x + size
            || pos.y > screen.y + size) {
        // 画面外に出た.
    }

if文の条件式が「||」演算子で連結された長い条件式となっています。

長い条件式は、別途 bool を返す関数にすると読みやすいコードとなります。

// 画面外に出たかどうか.
bool isOutSide(Vec2 pos, float size) {
    // 画面サイズ.
    Size screen = getScreenSize();
    
    if(pos.x < -size) {
        return true; // 画面外(左).
    }
    if(pos.y < -size) {
        return true; // 画面外(上).
    }
    if(pos.x > screen.x + size) {
        return true; // 画面外(右).
    }
    if(pos.y > screen.y + size) {
        return true; // 画面外(下).
    }
    // 画面内.
    return false;
}

呼び出し側はかなり短いコードとなります。

    // 現在座標.
    Vec2 pos = getPlayerPos();
    // オブジェクトの大きさ.
    float size = 16.f;

    // 画面外に出たかどうか.
    if(isOutSide(pos, size)) {
        // 画面外に出た.
    }

この修正もコードの行数は増えてしまいますが、処理が明確になって理解しやすく条件式に変更があった場合に修正がやりやすくなります。

📌条件式を関数化するメリット
  1. 条件式が関数に置き換えられることで、何をしているのかがわかりやすくなる
  2. 条件を変更したいときに修正がやりやすくなる

3. 長いswitch-case文を関数化する

switch-case文で状態に合わせた処理を行う場合、処理を細かく関数化してコードが長くならないようにしておきます。

// 状態.
enum eState {
    eState_FadeIn, // フェードイン.
    eState_Main, // メインゲーム.
    eState_Gameover, // ゲームオーバー.
    eState_FadeOut, // フェードアウト.
    eState_End, // 終了.
};

// 状態.
eState m_State = {};

// 更新処理.
void Update() {
    switch(m_State) {
    case eState_FadeIn:   { _UpdateFadeIn();   break; }
    case eState_Main:     { _UpdateMain();     break; }
    case eState_Gameover: { _UpdateGameover(); break; }
    case eState_FadeOut:  { _UpdateFadeOut();  break; }
    case eState_End:      { _UpdateEnd();      break; }
    }
}
📌switch-case文の処理を関数化するメリット
  1. 状態ごとの処理がわかりやすくなり、処理の修正がやりやすくなる
  2. 状態の追加がやりやすくなる

また switch-case文に限らず、1つの関数の処理が長くなる場合には細かく関数を定義(サブルーチン化)した方が関数の見通しが良くなります。

// 更新処理.
void Update() {
    // ゲームの処理.
    _UpdateGame();

    // カメラの更新.
    _UpdateCamera();

    // UIの更新.
    _UpdateUI();

    // BGMの更新.
    _UpdateBGM();
}

4. テーブルで処理をシンプルにする

テーブルを使うと条件分岐をシンプルに書くことができます。

例えば先程の switch-case文は以下のように関数テーブルで実装できます。

// 状態.
enum eState {
    eState_FadeIn, // フェードイン.
    eState_Main, // メインゲーム.
    eState_Gameover, // ゲームオーバー.
    eState_FadeOut, // フェードアウト.
    eState_End, // 終了.

    eState_Max,
};

// 状態.
eState m_State = {};

// 関数の型を定義.
using UPDATE_FUNC = std::function<void()>;

// 配列のサイズを取得するマクロ.
#define ARRAY_SIZE_OF(a) (sizeof(a)/sizeof(a[0]))

// 更新処理.
void Update() {
    // 更新関数テーブル.
    // 状態が追加されたらここに関数を追加するだけで良い.
    UPDATE_FUNC tbl[] = {
        _UpdateFadeIn,
        _UpdateMain,
        _UpdateGameover,
        _UpdateFadeOut,
        _UpdateEnd,
    };
    // 静的に配列サイズをチェック.
    static_assert(ARRAY_SIZE_OF(tbl) == eState_Max, "error: invalid tbl size");

    // 関数テーブルから呼び出す.
    tbl[m_State]();
}

switch-case文がなくなり、代わりに tbl という配列が追加されました。switch-case文よりも少しシンプルになっています。この書き方は追加があったときに配列に追加するだけでよくなるので、とてもシンプルな書き方となります。

なおC++の場合は配列に関数ポインタを登録できるのでこのような書き方となっています。別の言語の場合はラムダ式などで登録することになるかもしれません。

ただし、この書き方は配列の特定のインデックスへ直接アクセスする危険性や、呼び出し関数がコードを見ただけではやや分かりづらいというデメリットもあるため、やや注意が必要です。

他のテーブルの使用例としては、例えば落ちものパズル上下左右のタイルを調べるようなケースです。

    Point2 pos(5, 4); // 基準の座標.

    // 上下左右のテーブルを作る.
    Point2 tbl[] = {
        Point2(-1, 0), // 左.
        Point2(0, -1), // 上.
        Point2(1, 0), // 右.
        Point2(0, 1), // 下.
    };

    // 上下左右のタイルを調べる.
    for(auto v : tbl) { // C++のイテレーター.
        // 基準座標に足し込む.
        Point2 p = pos + v;
        // pが上下左右の位置となるので
        // その位置に対応するチェック処理を行う.
        cout << "(" << p.x << "," << p.y << ")" << endl; 
    }

 

落ち物パズルでのタイル座標系における「上下左右」は連続性がない値なので、このようにテーブルを定義してイテレーターを回すことで条件分岐を排除できます。

📌テーブル化することのメリット
  1. 条件分岐を排除することができる
  2. テーブルに追加するだけで新しい機能を追加できる

5. 配列へのアクセスは関数を経由する

特にC/C++言語の場合は、配列の領域外アクセスはメモリを壊す可能性があるので、Get〜() / Set〜() といった関数を用意して安全に配列にアクセスするようにします。

const int MAP_WIDTH = 12; // マップの幅.
const int MAP_HEIGHT = 8; // マップの高さ.

// マップデータ.
int m_Map[MAP_HEIGHT][MAP_WIDTH] = {};

// 有効なマップ座標かどうか.
bool CheckValidMapPos(int x, int y) {
    if(x < 0 || MAP_WIDTH <= x) {
        return false; // 領域外アクセス.
    }
    if(y < 0 || MAP_HEIGHT <= y) {
        return false; // 領域外アクセス.
    }
    return true; // 正しい場所.
}
// マップの値を取得する.
int GetMapData(int x, int y) {
    if(CheckValidMapPos(x, y) == false) {
        return -1; // 領域外アクセス.
    }
    return m_Map[y][x];
}
// マップに値を設定する.
void SetMapData(int x, int y, int v) {
    if(CheckValidMapPos(x, y) == false) {
        return; // 領域外アクセス。ASSERTで止めても良いかも.
    }
    m_Map[y][x] = v;
}

マップデータは幅 "MAP_WIDTH"高さ "MAP_HEIGHT" の配列として確保されているので、GetMapData() / SetMapData() では CheckValidMapPos() で範囲チェックを行ってから処理を行うようにしています。

これにより配列で確保したメモリ領域を超えた部分へのアクセスができないようになっています。

C#などでは領域外アクセスはエラーとして検知されるため、こういったチェック処理は必要ないようにも思えますが、領域外をエラーではなく何もしないようにしたい、といった場合には関数を経由してアクセスすると呼び出しがシンプルにかけることがあります。

📌配列へのアクセスを関数化するメリット
  1. メモリを壊す心配がなくなり安全に配列にアクセスできる
  2. 領域外アクセスをエラーにするか無視するかを自由に決められる

6. インデントは深くしない

ゲームプログラムを書くときには勢いが大事だったりするので、勢いでインデントが深いコードを書いてしまいがちです。

// オブジェクトリスト.
Object m_ObjList[8] = {};

...
    // チェックしたい情報.
    int id = 5; // IDが5であるもの.
    float px = 100; // X座標が100以上.
    float py = 500; // Y座標が500以下.

    for(auto obj : m_ObjList) {
        if(obj.m_Exists) {
            // 存在している.
            if(obj.m_Id == id) {
                // IDが一致.
                if(obj.m_PosX >= px) {
                    // 座標が100以上.
                    if(obj.m_PosY <= py) {
                        // 座標が500以下.
                        // 見つかった.
                        cout << "match!" << endl;
                    }
                }
            }
        }
    }

実装できるまではこの書き方でも良いのですが、ある程度、実装のメドが立ったらこのインデントの深さを直していきます。

方法としては、多くのパターンでは「ガード節を使う」「関数化する」の2つです。(場合によってはローカル変数にしたりテーブル化するテクニックも使えます)。

1. ガード節を使う

例となるコードでは、条件が「」となる場合にインデントが1つずつ深くなっています。これを逆に考えて「条件が『』だったら continueする」という処理に置き換えます。

するとインデントの深さが増えずに読みやすいコードとなります。

// オブジェクトリスト.
Object m_ObjList[8] = {};

...
    // チェックしたい情報.
    int id = 5; // IDが5であるもの.
    float px = 100; // X座標が100以上.
    float py = 500; // Y座標が500以下.

    for(auto obj : m_ObjList) {
        if(obj.m_Exists == false) {
            continue; // ガード節.
        }

        // 存在している.
        if(obj.m_Id != id) {
            continue; // ガード節.
        }

        // IDが一致.
        if(obj.m_PosX < px) {
            continue; // ガード節.
        }

        // 座標が100以上.
        if(obj.m_PosY > py) {
            continue; // ガード節.
        }

        // 座標が500以下.
        // 見つかった.
        cout << "match!" << endl;
    }

2. 関数化する

そもそも for文の中で多くの条件分岐を入れていることが問題と考えると、チェック用関数を作って判定するのも良いと思います。

// オブジェクトリスト.
Object m_ObjList[8] = {};

// オブジェクトが指定の条件にマッチしたかどうか.
bool isMatchObj(Object& obj, int id, float px, float py) {
    if(obj.m_Exists == false) {
        return false; // 存在していないのでチェック不要.
    }

    if(obj.m_Id != id) {
        return false; // IDが一致していない.
    }

    // IDが一致.
    if(obj.m_PosX < px) {
        return false; // X座標が指定の値よりも小さい.
    }

    // 座標が100以上.
    if(obj.m_PosY > py) {
        return false; // Y座標が指定の値よりも大きい.
    }

    // 条件にマッチした.
    return true;
}

...
    // チェックしたい情報.
    int id = 5; // IDが5であるもの.
    float px = 100; // X座標が100以上.
    float py = 500; // Y座標が500以下.

    for(auto obj : m_ObjList) {
        if(isMatchObj(obj, id, px, py) == false) {
            continue; // ガード節.
        }

        // 見つかった.
        cout << "match!" << endl;
    }
📌ネストを深くしない方法を使うメリット
  1. 条件に対応する処理がわかりやすくなる
  2. 新しく条件を追加するのがやりやすくなる

7. スコープの大きさに適した名前をつける

これは中規模以上のゲームを作るときの話かもしれませんが、スコープの大きさに合わせて適切な変数名・関数名をつけるようにします。

例えばターン制のコマンドバトルRPGを作る場合、戦闘中のキャラクターを管理するクラス名を"Character"というクラス名にしてしまうと フィールド探索中のキャラクターなのかバトル時のキャラクターであるかがわからなくなります。

そのため、例えばフィールド探索中のキャラクター(NPCなど)であれば "FieldCharacter"バトル中のキャラクターであれば "BattleCharacter" といったように適切な接頭語をつけることで区分できるようにしておきます。

もちろんキャラクターがバトル中にしか登場しないのであれば、"Character" というクラス名でも問題ありませんし、名前空間で分けることができるなら短い名前でも問題ないと思います。

さらにスコープが限定的であれば、1文字変数もありです。

// 所持金を加算.
void AddMoney(int v) {
    m_Money += v
}

引数の変数名を "money" にしても良いですが、ここでの加算値はお金しかありえないので "v" (value) といった短い変数名の方が読みやすいケースがあります。

その他細かいテクニック

公開するメンバ変数や関数は限定的にする

クラス内の変数を直接外部から変更できてしまうと、どこで書き換わっているのかがわかりにくくなります。

プログラム言語にもよりますが、private宣言が使えるならそれを定義してアクセス可能な変数や関数を限定的にすると良いです。

typo(スペルミス)を避ける

例えば "Enemy" を "Enmey" と typo(スペルミス) すると、ワード検索した時に見つからないという問題が発生します。(クラス名であればコンパイルエラーで見つかりますが、変数名の宣言だとエラーになりません)

変数名・関数名の宣言には typo の罠が潜んでいる…ということに注意すると良いです。また英単語を使うときに間違った綴りを使うのはエラーではないですが、複数人で開発するときに他のメンバー可読性の低下にもつながるので、使い慣れない英単語を使う場合は Google検索で綴りを調べてから使うと良いです。例えば "Actor" を "Acter" と間違って書いてしまうなどです。

さらに「防御」を意味する英単語は "defence" と "defense" の2種類存在します。前者はイギリス英語で後者はアメリカ英語となり意味自体は一緒なのですが、プロジェクトによってどちらを使うかを見極めた上で使用することが大切となります。

適切なコメントを入れる

個人的にはコメントは少ないよりも多いほうが良いと考えています。ただコメントを多くすると保守が大変なので少なくしたほうが良い、という意見もあって正解はありません。

ただどちらであっても言えることとしては、「間違ったコメント」や「わかりにくい文章」のコメントだと読んだ人が理解するまでに時間がかかってしまいます

ここの塩梅は難しいところですが、参考としてタワーディフェンスを作ったときのコードをのせておきます(タワーの配置の処理。このコードだけ Godot Engine の GDScirptです)。

改めて見るとあまりきれいなコードではないですが、for文やif文など処理の区切りにコメントを入れる、後から処理が変更されるかもしれないところなどに注意してコメントを入れるようにしています。

## 更新 > ビルド(配置).
func _update_build() -> void:
    # カーソルの更新.
    _ui_cursor.position = Map.get_mouse_pos(true)
    _ui_cursor.visible = true
    # 配置できるかどうか.
    var mouse_grid_pos = Map.get_grid_mouse_pos()
    ## 地形をチェック.
    var cant_build = Map.cant_build_position(mouse_grid_pos)
    ## タワーのチェックも必要.
    for tower in _tower_layer.get_children():
        var grid:Vector2i = Map.world_to_grid(tower.position)
        if grid == mouse_grid_pos:
            # 置けない.
            cant_build = true
            break

  # 配置できないカーソルの状態を更新.
    _ui_cursor_cross.visible = cant_build
    _ui_cursor_tower.visible = (cant_build == false)
    if cant_build == false:
        _ui_cursor_tower.visible = true
    
    # タワーの生存数をカウント.
    var num = 0
    for tower in Common.get_layer("tower").get_children():
        var t:Tower = tower
        if t.get_type() == _buy_type:
            num += 1 # 一致する種別のみ.
    
  # タワーのコストを取得.
    _buy_cost = Game.tower_cost(num, _buy_type)
    if Common.money < _buy_cost or Input.is_action_just_pressed("right-click"):
        # お金足りない or キャンセル.
        _cancel_build()
    elif Input.is_action_just_pressed("click"):
        if cant_build:
            print("ここには建設できない")
        else:
            # ビルド実行.
            _exec_build(_buy_cost)

良いコードを書くために参考となる本

最後に良いコードを書くために参考となる2冊の本を紹介しておきます。

リーダブルコード

多くの人がおすすめしている本なので、あえて紹介する必要はないかもしれませんが、「リーダブルコード」は手元に置いてて損はない、一生使える名著だと思っています。

リーダブルコードは、コードを読みやすくする数多くのテクニックやヒントが詰まっているので、定期的に読み返すと新しい発見があってとても良い本だと思っています。

リファクタリング

こちらも有名どころです。コードの動きを保ったままキレイなコードにするテクニック「リファクタリング」を徹底的に解説した本で、リーダブルコードとはまた違った知見が得られます。

リファクタリングとはそもそも何なのか、リファクタリングを行うべきタイミングである重複したコード」「長すぎる関数」「巨大なクラスについて、逆にリファクタリングをするべきでないタイミング、リファクタリングを行いやすくなる環境作りはどうするべきか、などなど…。

かなり分厚いので読み物というよりも、コードを整理したいときにやりたいことに合わせて辞書的に使える本だと思います。

PythonでExcelをCSVに書き出す方法

今回は Excelファイルを CSVに書く出す方法について解説します。

PythonExcelCSVに書き出す方法

Excelでゲームデータを管理するときの問題点敵のパラメータを管理する場合、表計算ソフトである "Excel" を使用すると楽に管理ができます。例えば敵のパラメータのデータを以下のように定義するとします。

Excelデータは通常「.xlsx」で保存しますが、このデータはバイナリデータであるため実行環境(ゲームエンジンなど)で解析するのは少し大変なので、通常は「.csv」などのテキストファイルに変換した上で扱うこととなります。

ここで問題となるのが、書き出しを行うたびに「エクスポート」の操作を行う手間が発生することです。

できることなら変更した後は簡単に CSV に書き出せるようにしたいので、Pythonを使って書き出せるようにします。

Pythonから Excelのデータを書き出す

openpyxlのインストール

Pythonがインストール済みであれば、"openpyxl" というライブラリを使用することで簡単に CSV への書き出しができます。

"openpyxl" のインストールは以下のコマンドでインストールできます。

コマンドプロンプト
pip install openpyxl

CSV書き出しのスクリプト

早速ですが、CSV書き出しのスクリプトは以下のものとなります。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# ===============================================
# xlsxをcsvに出力するツール
# ------------------------------------------
# openpyxl を使用するには以下のコマンドを使います
# >pip install openpyxl
# ===============================================
import openpyxl
import sys

def usage() -> None:
    print("usage:")
    print(" xlsx_to_csv.py [.xlsx] [.csv]")

def main() -> None:
    argv = sys.argv
    # テスト用の引数
    #argv = ["", "enemy.xlsx", "enemy.csv"]    

    if len(argv) < 3:
        usage()
        return
    
    xlsx_path = argv[1]
    csv_path = argv[2]

    # 読み取り専用でExcelを開く.
    wb = openpyxl.load_workbook(filename=xlsx_path, read_only=True, data_only=True)
    # シートオブジェクトを取得.
    sheet = wb["Sheet1"]

    tmp = ""
    for i in range(sheet.max_row):
        row = i + 1 # Excelは1始まり
        v = sheet.cell(row, 1).value
        if v is None:
            break # 最初の列が空欄であれば終了とする

        for j in range(sheet.max_column):
            col = j + 1 # Excelは1始まり.
            v = sheet.cell(row, col).value
            if v is None:
                v = "" # 空欄の場合は空文字にしておきます
            if j > 0:
                tmp += "," # 1列目でなければカンマを入れる
            tmp += str(v) # 文字列にします
        tmp += "\n" # 改行
    print(tmp)

    # xlsxを閉じる.
    wb.close()

    # CSVに書き込む.
    csv = open(csv_path, "w")
    csv.write(tmp)
    csv.close()

if __name__ == "__main__":
    main()

簡単に説明すると openpyxl.load_workbook()Excelファイルを読み込みます。読み込みに成功するとWorkBookオブジェクトを返すので、そこからWorkSheetオブジェクトを取り出します。

WorkSheetオブジェクトには cell() という関数が用意されているので、それを使ってセルのデータを取り出します。なお空欄の場合は None となるため、それをデータの終端として判定できます。

このスクリプトコマンドプロンプトから以下のようにして呼び出します。

コマンドプロンプト
python xlsx_to_csv.py enemy.xlsx enemy.csv

完成スクリプトデータ

今回作成したスクリプトとサンプルデータを添付しておきます。

"conv.bat" (バッチファイル) は Windows環境でのみ使用することができます。

グリッド制のゲームでよく使う座標系について

今回は Grid-based (グリッド制) のゲームを作るときによく使われる座標系とその変換方法について書きます。

グリッド制のゲームでよく使う座標系について

グリッド制のゲームとは

グリッド制は、パズルゲームやターン制ストラテジー、昔ながらのターン制ローグライクゲームなどでよく採用されます。例えば以下のようなゲームです。

グリッドというマス目で区切られた単位でゲーム画面が構成されるので、思考が必要とされるパズルゲームやストラテジー要素があるゲームに向いています。

このような「マス目」を1つのデータの単位として扱うときに役立つ座標系とその変換方法について説明します。

グリッド制のゲームを作るときに便利な3つの座標系

グリッド制を採用したゲームでは、以下の3つの座標系を使用すると便利です。

  • 1. ワールド座標系: 実際に画面にオブジェクトを表示する座標系。今回はスクリーン座標系と同義
  • 2. グリッド座標系: グリッドを基準とした2次元の座標系
  • 3. インデックス座標系: グリッド座標系を「1次元」に変換した座標系

なおグリッドの単位を「タイル」と呼ぶこともあります。これは Tile-matching game (タイルマッチングゲーム) で1つのパネルの単位をタイルとしている影響もあります。ところどころ「タイル」という名称を使っている部分もありますが、その場合はグリッドと読み替えてもらえればと思います。

1. ワールド座標系とは

ワールド座標系とは、ゲーム画面に表示されている実際の座標です。スクロールがないゲームであれば、スクリーン座標系と同じと考えて問題ありません。

ワールド座標系は主に「左上の開始位置」「各グリッドのサイズ」の2つの要素で構成されます。

例えば、上記の画面構成の場合、タイルの描画開始座標が (x, y) = (32, 32) となっていて、1つのタイルサイズが 64x64 です。

そのため、例えば黄色で囲んだタイルの座標は以下の計算で求められ、ワールド座標は (x, y) = (288, 96) の位置に存在します。

2. グリッド座標系とは

グリッド座標系とは、タイルをグリッド基準で考えたときの座標系です。

以下の白い四角の部分は、X方向では "4"、Y方向では "1" の場所にあるので、グリッド座標系では (gx, gy) = (4, 1) の位置に存在するといえます。

📌グリッド座標系を使用する理由

なぜグリッド座標系を使用するのかというと、タイルの消去判定が楽に行えるためです。

ワールド座標系だと、例えば隣にあるタイルを調べるためにタイルのサイズぶん位置を移動する必要があります (今回であれば 64px)。

それに対してグリッド座標系では、1つ移動するだけで隣のタイルを調べることができます。

といったように「プログラム上での情報の扱いやすさ」がグリッド座標系を採用する理由です。

そしてグリッド座標系を使用することで得られる、もう一つのメリットが「すべてのタイル情報を2次元配列で扱うことができる」ことです。

タイルの種類にIDを割り振ると、2次元の数値の配列でフィールド情報を表現でき、消去判定がやりやすくなります。

3. インデックス座標とは

インデックス座標系とは、グリッド座標系を1つの単位だけ(通し番号)で管理できるようにしたものです。

例えば、グリッド座標系で (x, y) = (4, 1) の位置は、インデックス座標系では「12となります。

インデックス座標系は「1つの数値」で表現できるので、数値の1次元配列で管理できるのがメリットです。

それぞれの座標系の相互変換

それぞれの座標系は相互に変換が可能です。

それぞれの計算式の例です。

## ワールド座標系をグリッド座標系に変換する (X).
func world_to_grid_x(wx:float) -> int:
    # 左上のオフセット座標を引いてタイルサイズで割る
    var gx = int((wx - OFS_X) / TILE_SIZE)
    return gx

## ワールド座標系をグリッド座標系に変換する (Y).
func world_to_grid_y(wy:float) -> int:
    # 左上のオフセット座標を引いてタイルサイズで割る
    var gy = int((wy - OFS_Y) / TILE_SIZE)
    return gy
## グリッド座標をインデックス座標に変換する.
func grid_to_idx(gx:int, gy:int) -> int:
    # グリッド座標(Y)のフィールドの横幅をかけて、グリッド座標(X)を足し込む
    return (gy * FIELD_WIDTH) + gx
## インデックス座標をグリッドX座標に変換する.
func idx_to_grid_x(idx:int) -> int:
    # グリッド座標(X)はフィールドの横幅の剰余で求められる
    return idx % FIELD_WIDTH

## インデックス座標をグリッドY座標に変換する.
func idx_to_grid_y(idx:int) -> int:
    # グリッド座標(Y)はフィールドの横幅で割ることで求められる]
    return int(idx / FIELD_WIDTH)
## グリッド座標系をワールド座標系に変換する (X).
func idx_to_world_x(gx:int) -> float:
    var wx = OFS_X + (TILE_SIZE * gx)
    return wx

## グリッド座標系をワールド座標系に変換する (Y).
func idx_to_world_y(gy:int) -> float:
    var wy = OFS_Y + (TILE_SIZE * gy)
    return wy

ちなみにサンプルコードでは、グリッド座標系を「整数値」として扱いましたが、グリッド間の移動をなめらかにする場合には、グリッド座標系を小数値で扱ってしまって良いと思います。

例えば以下の記事で解説したマッチ3ゲームでは、グリッド座標系を小数値で処理しています。

関連記事

Godot Engine の A* はグリッド座標系とインデックス座標系を扱った実装となるので、今回の内容を理解するとこちらの実装も容易となると思います。

A* アルゴリズムを自作する場合には以下の記事が参考になるかもしれません。

落ち物パズルでは今回の考え方を知っておくことで、楽に実装できます。

ストラテジーの移動範囲の求め方や経路探索にも使えます。

 

Kindle Unlimitedで読めるゲーム開発に役立つ本

今回は電子書籍が読み放題になるサービス Kindle Unlimited で読めるゲーム開発に役立つ本についてまとめてみました。

 

📌Kindle Unlimited は定期的に割引キャンペーンを行います

Kindle Unlimited は定期的に割引キャンペーンを行っているので、その期間だけ契約するのがおすすめです。

 

📌Kindle Unlimited の対象本は入れ替わります

Kindle Unlimited の対象となる本は配信する人・会社の都合で入れ替わります。そのため、ここで紹介した本が Unlimited の対象かどうかは以下のマークで判断できます。

なお、ここで紹介した Kindle本は基本的に Kindle Cloud Reader (Webブラウザ) では読めない本がほとんどなので、Kindle アプリ をインストールする必要があります。

ゲーム開発初心者向け

0から副業でゲームアプリの個人開発をやってみたい初心者が、プログラミング言語の学習を始める前に読むべき超入門書

こちらはゲームを作ったことがない、ゲームプログラムをしたことがない、ゲームで稼ぐにはどうすればよいのか、といった初歩的な疑問に対する答えがまとまっている本です。

また「500円の価値がない」というレビューにもある通り、確かにネットで調べればわかる情報が多いです。ただ Kindle Unlimited を利用すると実質無料で読めますし、ネットで調べる手間を削減できるので、右も左もわからないゲーム開発初心者には良い本ではないかと思いました。

ゲーム開発を勉強するにあたって必要な情報がまとまっており、おすすめのツールや本が紹介されているので、「ゲームを作りたいけれど何をすればわからない」という人にオススメです。

これからゲームアプリを 作ろうと思っている人へ 伝えたいこと: 「ただなんとなく・・・」ゲームアプリ制作に興味を持った方へ

Hyper-Casual game (ハイパーカジュアルゲーム) を中心に100本以上のゲームを作り、最高月収120万円を達成した著者によるゲーム開発初心者にむけた本となります。

この本では、GameSalad というプログラムコードなしで作れる開発環境を紹介しており、初心者でも入りやすくゲーム開発のきっかけとして良い本だと思いました。

なお「本当に月収100万円もあるのかな…?」と疑問に思って作者さんのアプリを調べてみたのですが、こちらのアプリの開発者のようで、10万〜100万DLを達成しており、たしかにこれだけダウンロードされていれば、それだけの収入がありそう…と思いました。

ゲームプログラム

ゲームを作りながら楽しく学べるPythonプログラミング

Pythonの基礎をゲームを作りながら学べる本です。Pythonは文法がわかりやすく覚えやすいので、プログラム初心者にオススメの言語なのですが、メインとして使わなくても開発を補助するツールとしても使える(データコンバートの自動化など)ので、UnityやUnreal Engineなど他の開発環境を使用していても覚えておいた方が良いと思っています。

また、Unreal Engine のエディタ拡張のスクリプト言語としても Python が採用されています。

この本の前半では Python の文法とプログラムの基礎を学び、後半で実際にパズルやシューティングゲームを作りながらゲームづくりの基礎を学べる本となっています。

出典:ゲームを作りながら楽しく学べるPythonプログラミング

ただ注意点として、Pygame を使っており、macOS環境だと環境構築がやや難しいです。Windows環境を使用していれば問題なく構築できると思います。

Unityの寺子屋 定番スマホゲーム開発入門

Unityを使っているなら役に立つ本だと思います。

全くの初心者にはやや少し難しいかもしれませんが、「 Incremental game (放置系ゲーム) 」「 Cookie Clicker (クッキークリッカー) 」「 2D Platformer (プラットフォーマー) 」の作り方を解説した本です。

出典:Unityの寺子屋 定番スマホゲーム開発入門

特に「放置系ゲーム」「クリッカー系ゲーム」はスマートフォン向けのゲームとして人気が高く収益性が高い(継続率が高い)ことで知られていますので、ゲームで生計を立てていきたいと考えている人には良い題材ではないかと思います。

[Unite]個人ゲームアプリ開発者はいかにして生きていくのか? 「和尚」が語るゲーム作家の生き方指南
出典:[Unite]個人ゲームアプリ開発者はいかにして生きていくのか? 「和尚」が語るゲーム作家の生き方指南
ゲームタイプ 1DLあたりの収益 補足
脱出ゲーム 3〜12円 リプレイバリューが低い
(繰り返しプレイされない)
ので収益率は低め
カジュアルゲーム 5〜25円  
放置ゲーム 8〜42円  
クリッカー 10〜50円 アプリ内課金を組み合わせれば
1DLあたり100円を超えることもある

Unity入門の森シリーズ

初心者向けのUnityのチュートリアル記事を書かれている「Unity入門の森」の方の Kindle本です。

とても丁寧に説明されている良い本なのですが、Webサイトにもほぼ同じ内容の記事を書かれているので、個人的な感想としてはKindle本よりもWebサイトの方が見やすいような気がしました。

プランニング(業界・チーム開発)

プランナー一年生のためのゲーム仕様書の書き方

ゲーム開発会社の「サクセス」のプランナー向けの社内教育用資料をまとめた本となります。

内容としては「ゲームの仕様書」の書き方で、プログラマーにゲームの仕様を共有するための資料をどうやって作成するかが書かれており、かなり実践的な本となっており、業界を目指している方は読んでおいて損はないと思います。

ゲームプランナーのチーム運営例: ~ゲーム性以前の問題を発生させないために~ ゲームプランナーの参考例シリーズ

ゲーム業界歴20年以上のキャリアを持つプランナーの方の本となります。

こちらはゲーム開発における「プロジェクトマネジメント」についての本で、炎上するプロジェクトのあるあるがよくまとまっていて、私もマネジメント経験があるので読んでいて胃が痛くなる本でした。

それだけリアリティがあり、マネジメントを任されたときにどういった立ち回りをするべきなのか、どういう準備をすればよいのかがまとまっていて、管理職に就きたい方にはおすすめの本だと思います。

この方は他にも「ゲームプランナーのドキュメント作成例<基本編> ゲームプランナーの参考例シリーズ」「ゲームプランナーのミーティング実践例 ゲームプランナーの参考例シリーズ」という本を書かれおり、どちらも実践的な内容でおすすめです。

ゲーム企画職の歩き方 - ゲーム開発会社に就職してからの手引き

ゲーム開発全般

ゲーム開発者の地図: 20年の個人開発から学んだこと

ゲーム開発を20年以上続けており、RPGを作るエディターとして人気の高い「WOLF RPGエディター」の開発者でもある SmokingWOLF さんのゲーム開発ノウハウをまとめた本です。

ゲーム開発者全員におすすめできる本ですが、特にRPGを作成している方にはRPG特有のパラメータ調整方法や魅力的なキャラクター作りの方法は参考になると思います。

2022.8.6 追記:RPGゲームデザイン奮闘記 ー5つのネイトの作り方 ー

「ねこどらソフト」による、5属性がころころ変化するRPG「5つのネイト」が完成するまでの経緯をまとめた本です。

この本の著者は、RPGを初めて作ってみたということもあり、色々な紆余曲折があって、そのあたりを包み隠さず書かれていて、RPGを作ったことがない方にはとても参考になると思います。

また、5つのネイトを遊んでみるとわかるのですが、ボス戦ごとに遊ばせ方がどんどん変化して、手作りの良さが感じられるゲームとなっていて、ボス戦での専用ギミックの作り込みとバリエーションが多く、バトル中心で進むゲームを作りたい方におすすめなのではないかと思います。

自作ゲーライフ: 個人スマフォゲームアプリのマネタイズ本

スマートフォン向けのゲームを作っている個人ゲーム開発者向けのマネタイズ(収益化する方法)について書かれた本です。実際にゲーム開発で生活できており、収益率の高いアプリや人気のあるアプリを作られている方なので説得力があります。

また、マネタイズやプロモーションだけでなく、ゲームを完成させるための方法や、開発の悩み解決法、はたまた食事について書かれているのが面白いです。

経験ゼロでも作れるフリーホラーゲーム。制作の流れ・ストーリーの構成・ギミックに必要な3要素。20分で読めるシリーズ

📌Unlimited 対象から外れました

2023.1.7 追記:こちらとても良い本なのですが、Unlimited 対象から外れてしまったようです

最近はホラーっぽいゲームを作っているので参考になるかな…と思って読んでみたのですが、なかなかよい本でした。内容としては初歩的なストーリー作成や企画、謎解き、分岐やギミックについて、テンプレとなる分類がされており、ホラーゲームを作り慣れていなければ参考になるかと思います。

例えば、ホラーゲームのシチュエーションとしては、以下の3つが典型的なパターンとして提示されています。

  • 1. モンスターからの逃亡:未知の化け物や猛獣、人間の業より生まれた怪物、快楽殺人者から逃げる
  • 2. 環境からの脱出:沈みかけている船、毒ガスが満ちていく閉鎖空間、命がけのゲームからの脱出
  • 3. 過去に触れる:過去の忌まわしい出来事の真相を知ってしまい主人公に危機が迫る

またそれぞれのパターンについての作成のコツが書かれており、参考になります。他にもギミックの作り方の注意点など、たしかにそれに注意しないと良くない作りになるなぁ……と勉強になりました。

個人的には最後の「ホラーゲームでやってはいけないこと」が参考になりました。

  • "予兆"以外のびっくり系イベントは多用しない:「 Jump scare (ジャンプスケア・ホラーゲーム) =びっくり系イベント」を多用すると飽きられる
  • 宗教を否定しない:否定的な主張があると炎上の可能性がある
  • 死は控えめに:死亡エンドが多いと怖さがなくなる。BAD ENDが多いと難しいゲームになる

一生忘れない怖い話の語り方 すぐ話せる「実話怪談」入門

こちらはホラーゲームのような「創作系」ではなく、現実にあった「実話系」の語り方の本ですが、創作系ホラーを作る場合にも怖さにリアリティを持たせるためにとても役に立ちます

ハイパーカジュアルゲーム入門: アプリリリースまでの考え方

Hyper-Casual game (ハイパーカジュアルゲーム) の基本について書かれた本です。ざっくりとハイパーカジュアルゲームについて知りたい、という方には参考になると思います。

以下の記事で参考にさせてもらいました。

ゲーム制作 現場の新戦略 企画と運営のノウハウ

📌Unlimited 対象から外れました

2023.1.7 追記:こちらとても良い本なのですが、Unlimited 対象から外れてしまったようです

前半部分は、ゲームの企画書を作る上で基本となる構成要素、企画が成立するまでのプロセス、オンラインゲームの運営方法などについて書かれています。

前半部分はどちらかと言えば、ゲーム業界に入りたい人向けの内容であると感じました。

そして、後半部分は実際にリリースされたゲームの企画がどうやって通ったのか、制作の過程はどのようなものだったか、どうやって売り出して運用したのか、といったことが書かれています。
取り上げられたタイトルは「ファンタシースターオンライン」「ダービースタリオンマスターズ」「魔界戦記ディスガイア」「CIRCLE of SAVIORS」「ソードアート・オンラインモリーデフラグ」「ダンガンロンパシリーズ」「Strange Telephone」「GUILTY GEAR Xrd REV 2」といった有名どころのタイトルばかりで読み応えがあったです。

個人的には、斜陽となっているアドベンチャーというジャンルを新しい切り口で売り出した「ダンガンロンパ」、独自の世界観と設定で話題になった「Strange Telephone」の制作過程についての記事は興味深かったです。

このプレゼンテーションが転機になりました。企画書を何度も作り直したとしても、アドベンチャーゲームというジャンルでは通らない。そう判断し、新しいジャンルを考えることからリスタートしました。話し合いを進める中で閃いたのが、アクション要素を入れること。そこで思いついたのが "ハイスピード推理アクション" という言葉です。そこからは、アドベンチャーゲームではなく、ハイスピード推理アクションと言い切るにはどうすればよいのかを考えながら、ゲーム内容を詰めていきました

ダンガンロンパ:アドベンチャーではなくハイスピード推理アクション

グラフィックがいまいちだと、ダウンロード数が伸びないですね。特に日本のユーザーの場合は顕著です。ゲーム内容がよいからといってグラフィックスをおろそかにしてしまうと、ダウンロード数が落ちてしまう今ヒットしているゲームを見ると、グラフィックスに、オリジナリティやクオリティが高いものが多いですね。特にゲームアプリのアイコンには気をつけないといけないと思っています。ほかにはないような独特のアイコンのデザインだと、端末に入れておくだけでもよいというファンもいます。ほかと差別化できるようなデザインのアイコンの付いたアプリは強いです。

Strange Telephone:ゲームにおけるグラフィックスの重要性

当たり前ですが、グラフィックで手を抜いてはダメですね。ストアでのスクリーンショットが、他のゲームよりも見劣りしてしまうとなかなか興味を持ってもらえないのではないかと思います。

2022.11.23 追記:ゲームデザイン力を育てる50の講義

ゲームデザイン初心者向けに書かれた本で、ゲームとして成立させるための基本となるスコアシステムやドットイート、アーケードゲームといった、シンプルなアイデアを中心したゲームデザインについて書かれた本です。

参考としているゲームのいくつかが90年代のニッチなゲームで、考え方が少し古いのが気になりますが、初心者向けとしては悪くないように思いました。

開発者インタビュー

石井ぜんじ「ゲームクリエイター」インタビュー集 ゲームに人生を捧げた男たち

カプコンの「岡本吉起」さん、元SNKの「足立靖」さん、ハムスターの「濱田愉」さん、Game in えびせんの「えび店長」さん、レイジングループの「amphibian」さん、千里の棋譜の「宮下英尚」さん、AIのスペシャリスト「三宅陽一郎」さん、プロ棋士でありコンピュータ将棋に詳しい「西尾明」さんのインタビュー本となります。

これらの方々に興味があるかどうかにもよりますが、興味があればゲームの作り方や考え方が学べるよい本だと思います。

イラスト関連

360°どんな角度もカンペキマスター! マンガキャラデッサン入門

こちらはキャラクターデッサンの基礎から学ぶことができる、お絵描き初心者向けの本です。とても説明が丁寧で最初に手に取る本としてもおすすめです。

プロ絵師の技を完全マスター キャラ塗り上達術 決定版

個人的に役に立った本です。キャラの塗り方のパターンがいくつかあって、私自身この本の塗り方を参考にしています。

ただこちらは定期的に見る資料となるので、私の場合はお金を払って購入しました。

絵の上達方法について、キャラクターイラストであれば、この本以外にもKindle Unlimited で読める本は多いので、これらをとりあえずダウンロードしてみて、好みのイラストなら模写してみる……という勉強法が良さそうです。

プロ絵師の技を完全マスター 魅せる背景 上達術 決定版

こちらは背景の塗りついての本となります。

月刊MdN 2014年 10月号(特集:イラスト表現の物理学 爆発+液体+炎+煙+魔法を描く)

参考になるデザインがたくさんある MdN のエフェクト特集です。

この号では、マンガやアニメ的なエフェクトのイラストの作りかたの特集があり、参考になります。この特集だけを目当てにすると高く感じますが、Kindle Unlimited なら読み放題で読めるので、そのあたりを気にせずに読めるのがとても良いです。

シナリオ

初心者でも完成させる 個人でのノベルゲーム制作の流れ: ノベルゲーム「Psychology King」を個人でゲーム開発したときの流れを実際の資料付きで解説

初心者でも必ず完成できるストーリーの作り方: この1冊でストーリーが生み出せるようになる、小説の書き方シリーズ第二弾! 基礎から応用まで!小説の書き方シリーズ

インディーズ小説家マニュアル」で初心者向けの小説の書き方やストーリーの作り方を解説されていて、その集大成となる本となります。

出典:我那覇アキラ

平易な文章で、できるだけお手軽にストーリーを作る様々な方法を数多く提示されていて「ストーリーが書きたいけど何をしたらいいかわからない…」という方にオススメの本となります。

大どんでん返し創作法: 面白い物語を作るには ストーリーデザインの方法論

物語の結末で読者をあっと驚かせる「どんでん返し」の 10パターンの方法と、その作り方を解説した本となります。

インパクトを与えるストーリー作りをしたい方には、その手法と物語構造を学べておすすめとなります。

ストーリー作家のネタ帳 イベント編1―キャラクターの王道プロット15種

個人的にシナリオ作成の参考にしまくっている「ストーリー作家のネタ帳」シリーズ。

他にもシナリオのパターンを分類されている方は多いですが、個人的にはこのシリーズが一番ではないかと思っています。

感情から書く脚本術

📌Unlimited 対象から外れました

2023.1.7 追記:こちらとても良い本なのですが、Unlimited 対象から外れてしまったようです

こちらはシナリオを作る上で知っておくべきことが数多く書かれた本です。個人的には「中級者〜上級者」向けの内容で、ある程度のお話を作れる人が「自分に足りないものは何か?」を知ることができるチェックリストのような本です。

個人的には、知りたいことを調べる「辞書」のような本だと思っていますので紙媒体の方が良いですが、Kindle版も持ち運びしやすいメリットがあるので補助的な資料として使っています。

この本は個人的にもかなりおすすめで、以下の記事でこの本を紹介していま

キャラクターからつくる物語創作再入門

こちらは魅力的なキャラクターの作り方について書かれた本です。ただ最近流行りの「属性」を組み合わせるタイプではなく「ストーリー」の中でキャラクターがどのように変化するのかを中心に書かれたものです。

そういったストーリーがどのように分類され、キャラの変化をどのように描けばいいのかが学べる本です。

資料

二次元世界に強くなる 現代オタクの基礎知識

アカシックレコード」や「ワルプルギスの夜」、「永久機関」や「タイムリープ」などオタク心をくすぐるキーワードについての資料となっており、ゲームアイデアやシナリオ作成に役立つ本ではないかと思います。

ただ説明は浅く広く、といった感じなので、あくまで用語集といった使い方でより詳しく調べる場合は Wikipedia などを参考にしたほうが良さそうです。

落ちものパズルの作り方

この記事では落ち物パズルゲームの作り方を解説します。

落ちものパズル

落ち物パズルとは、「テトリス」「ぷよぷよ」「ズーキーパー」「ツムツム」のようなゲームを表すジャンルです。落ちものパズルは、おおよそ以下のルールとなっています。

  • 1. 消去可能なブロックが上から落下してくる
  • 2. 一定ルール (同じ色をつなげるなど) でブロックの消去ができる
  • 3. 1〜2を繰り返して出現位置がブロックでふさがったり、一定時間内に消せないとゲームオーバー

通常、ブロックは下に遮るものがないと重力的な落下を行います。それによりフィールド上に配置されたブロックを瞬時に消すための思考力・判断力を求められるアクション性の高いパズルゲームとなっています。
(※ゲームによっては、非リアルタイムであったり、消去できないブロックが存在することもあります)

ズーキーパーの作り方

それでは、日本での Match-three game (マッチ3ゲーム) の代表とも言える、ズーキーパーの作り方を紹介します。

消去判定の作り方

落ち物パズルで重要なのが、ブロックをどのようにすると消すことができるのか、というルールの実装です。
このルールを実装するには、ゲームのフィールドに配置されているブロックの情報を2次元の配列で格納すると都合が良いです。
(後述しますが、この方法はツムツムを除きます)
例えば、ズーキーパーであれば、8x8のフィールドなので、8x8の二次元配列を用意します

int blockList[8][8] = {
    {1,2,4,6,5,2,3,7},
    {1,3,3,1,5,1,3,4},
    {4,2,5,1,1,6,7,1},
    {2,4,2,3,1,2,2,1},
    {7,6,5,4,2,1,1,5},
    {3,5,2,1,7,7,2,5},
    {7,5,3,1,7,2,5,2},
    {3,1,3,7,1,3,4,6},
};

上記のデータは、ブロックが以下のように並んでいるイメージです。

なぜ、2次元配列でブロックの管理をするのかというと、ゲーム画面での表示は、ブロックのサイズの間隔で配置されるからです。例えば、ブロックの画面上のサイズが 32x32 ピクセルとすると、左上から、(0, 0), (32, 0), (64, 0), ... という座標になります。この座標系では隣り合っているブロックを取得するには、少し面倒です。

そこで、2次元配列の座標系でブロックデータを扱うと、(0, 0), (1, 0), (2, 0), ... というように、データへアクセスすることができてわかりやすくなって、プログラム上で処理の都合が良くなります

ある程度プログラムをやるとわかることですが、「見た目の表示(=ユーザーインターフェース)」と「実際のデータ」との区別をしっかり行うと、役割分担が明確となり、プログラムの見通しが良くなります。

  • 見た目の表示:画面上でのブロック表示
  • 実際のデータ:2次元配列で管理しているブロックのデータ

では、この2次元配列を使って消去判定を行います。(x, y) = (4, 3) にあるブロックを (3, 3) に移動させると消せそうなので、この移動を行うとします。

すると以下のような配置になります。

このデータをもとに消去判定を行います。判定のアルゴリズムは、基準となる位置から「上下左右」に探索をするものとなります。

まず左から調べるとします。その場合、ブロックは違う色なのでこちらの方向には消すことができるブロックはない、という結果となります。

次に、上を調べます。すると同じ色なので消すことができる可能性があります。

なので、さらに上を調べます。

すると3つ同じ色が揃ったので消すことができます

消すことができるブロックの情報は直接ブロック情報を書き換えるのではなく、同じサイズの2次元配列を別に用意して、そこに格納します。

// 消去判定用の2次元配列データ
int blockEraseList[8][8] = {
    {0,0,0,0,0,0,0,0},
    {0,0,0,1,0,0,0,0},
    {0,0,0,1,0,0,0,0},
    {0,0,0,1,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0},
};

 

消去ブロックが複数ある場合は、このように情報を保持しておくと、余計な情報がないので消去処理が簡単にできます。このような感じで、上下左右を判定した後、消去対処が存在していれば消去処理を行います

blockEraseList の要素が “1” であるものを消します。(3, 1), (3, 2), (3, 3) が消去対象なので、それぞれの実データ(ゲーム画面に表示している側のデータ)を “0” に置き換えました。 

落下判定

消去ができたので、次は落下処理です。ブロックを落下させるために必要なのは、以下の情報です。

  • どのブロックを落下させるのか
  • ブロックをどこからどこへ落下させるのか

先ほどの図のような状態であれば、(3, 0) にある “6” を (3, 3) まで落下させる、ということを求める計算が必要になります。

方法としては、一番下の段にあるブロックから “0” のブロックを探します。そしてその段を調べ終わったら、1段ずつ上を調べていくようにします。

そして、 “0” を見つけたら、上方向を順番に調べます。先ほどの消去アルゴリズムと同様に、1マスずつ調べて、”0” でないブロックを見つけます。

上方向にブロックを見つけたので、以下の情報をどこかに格納しておきます。

  • 対象: “6” のブロック
  • 開始地点: (3, 0)
  • 落下地点: (3, 3)

落下情報をすべて作成し終わったら、この情報を使って落下アニメーションを行います。なお、落下アニメーションなしで、とりあえず動かしたいのであれば、1フレームで落下を完了させても問題ありません。
さきほどの、ブロックの並びでは問題ないのですが、状況によっては、落下ブロックのさらに上にブロックが存在することもあります。(というか、かなりの確率でそうなることが多いですね)

少し図を変更しました。”6” の上に “4” が存在しています。"4"も一緒に落下させる計算をするためには、落下計算用の2次元配列を別途用意します。そして、落下ブロックが存在した場合は、そちらに落下済みのブロックを移動させます。

これにより、その上にあるブロックの落下位置を正しく計算できるようになります。

処理のまとめ

ここまでの処理ステップを図にまとめてみました。

▼処理ステップ
1. ブロック操作を行い、ブロックを移動する
2. 消去判定を行う。
    a. 消去可能なブロックがあれば、3へ進む
    b. 消去ブロックが存在しない場合は、1へ戻る
3. 消去処理を実行してブロックをフィールドから消す
4. 落下可能なブロックがあれば落下処理を行い、落下完了したら 2に戻る

以上が、落ち物パズルの基本的な作り方となります。マッチ3パズルであれば、おおよそ、この流れで実装できます。テトリスは消去判定よりもブロックの回転処理が少し面倒ですが、基本は同じです。

テトリスの作り方については以下のページにまとめました。

Godot Engine でマッチ3パズルのサンプルを作ってみました。サンプルコードを公開していて、そのコードを解説する記事となります。

あと、ぷよぷよは、直線だけでなくジグザグにもつながるので、上下左右のつながりを連続で判定することになります。

ツムツムの作り方

ツムツムは他の落ちものパズルと比べてやや特殊な作りです。

ブロックが格子状に並んでおらず、箱にボールを入れたような状態になるため、やや複雑な衝突処理を実装する必要があります

ただ、物理エンジン」があれば、ブロック落下時の衝突処理が簡単にできるため、ツムツムの実装が容易となります。特にUnityでは簡単に物理エンジンを扱うことができるので、Unity初心者向けの題材としても良いです。
もちろん、衝突処理を自作するのもとても勉強になるので、プログラムに自信のある方にはそこから作ってみてもよいでしょう。

ひとまず、ここでは落下処理を物理エンジンで実装するとして、消去ロジックの実装方法を紹介します。

消去判定

まず、真ん中にある水色のブロックを選んだとします。

そうしたら、そのブロックを中心に円(距離)で一定の範囲内に同じ色のブロックがあるかどうかを判定して、距離内であれば接続可能、そうでなければ接続不可、という判定をします。


上と下にあるブロックは接続可能ですが、左上にあるブロックには届かないので接続できない、という判定になります。

ここでは、下にあるブロックに接続したとします。

そうしたら、接続したブロックから、さらに別のブロックに接続できるかをチェックします。この時、接続済のブロックは除外して判定を行います。そうして、スクリーンから手を離した時に、接続済のブロックを消去する、という判定で実装が可能です。

なお、消去可能な範囲 (距離) は適切な値に調整する必要があります。広くしすぎるとあまりにも消してやすくなってしまい退屈なゲームになってしまうかもしれません。逆に狭くしすぎると、なかなか消せなくてストレスのたまるゲームになってしまいます。

また、ブロック落下中にスワイプ操作でブロック同士を接続した場合、ブロックが落下して接続済のブロックとの距離が離れ過ぎてしまう問題があります。この場合は、その時点で強制的に消去処理に進むなど、不自然な接続にならないよう、特別な処理を入れても良いかもしれません。

落ちものパズルのシステムをどう展開していくか

落ちものパズルをより長く楽しませるには、どういう構成が良いかを考えてみます。
1つの案としては、すでにフィールドにブロックが配置されていて、それらを全て消すとステージクリアにするというものです。

それに対して、私がよく作るパターンとしては、ブロックをたくさん消すほど多くのダメージを敵に与えて、一定ダメージを与えると敵を倒せる、というルールを採用しています。
要はパズドラ方式ですね。

以下は昔 iPhone 向けに作った落ち物パズル「かずおち」です。

「たくさん消すと敵に大ダメージ」というシステムを採用すると、たくさん消すことがスコアという抽象的なものではく、敵へのダメージというわかりやすいものに反映されるため、手軽に爽快感が得られます。

また、敵はブロックを降らしたり、一定ターン数で攻撃するといった単純なAIで実装できて楽です。敵を強くしたければ、HPを増やしたり、厄介なブロックを登場させたりするだけで良いので、これまた難易度調整が容易にできてしまうのですよね。

ということで、特に面白くするアイデアが思いつかない場合は、このルールを採用すると、それなりに面白くなるのでオススメです。

もし、もっと長い開発期間をかけられるのであれば、ステージをクリアするごとにお金が手に入り、それで部屋の飾り付けなどができるコレクション要素を入れても良いかもしれません。

 

ラン&ジャンプ系ゲームの作り方

この記事では Jump ’n’ run game (ラン&ジャンプゲーム) の作り方を紹介します。

作り方手順

1. プレイヤーを表示する。重力で落下させる

まずはプレイヤーを表示して重力で落下させます。

重力の扱いを理解していれば、特に難しくないと思います。

2. 床を表示する。プレイヤーが床の上に乗れるようにする

床を表示して、プレイヤーがその上に乗れるようにします。

判定は「上からの当たり判定のみ有効にする」という方法で実装できます。ジャンプも実装して、ちゃんとジャンプ後に着地できるか確認しておきましょう。

3. プレイヤーの横移動を実装する。それに追従して画面をスクロールさせる

プレイヤーが横に移動し続けるようにします。そして、その動きに追従して、画面をスクロールさせます。

スクロールの考え方としては、「ワールド座標系」「スクリーン座標系」という2つの座標系の考え方を理解することです。

  • ワールド座標系:各オブジェクトが実際に存在する座標
  • スクリーン座標系:画面内に表示する座標

計算方法ですが、例えば、ワールド座標としてはプレイヤーをどんどん右に移動させて、プレイヤーの位置を 500px に移動したとします。

ただ、画面上では、プレイヤーは 80px の位置に表示させたいとします。

そうすると、スクリーン座標の左側は 500 - 80 = 420px にズラす必要があります。この値を元にプレイヤーだけでなく、床オブジェクトも実際の座標から 420px ズラすとスクロールが実装できます。

ワールド座標に存在するゲームオブジェクトをスクリーン座標に変換する

この考え方を実装することが難しい場合は、ひとまず床そのものを動かすという方法もありです(座標変換を行わずすべてスクリーン座標系で処理を行う)。その場合は、プレイヤーが右へ移動する処理は不要となります。

4. 床を動的に生成する。当たり判定が正しくできているかを確認する

床をプログラムから自動生成するようにします。ひとまず上記画面のようにランダムな位置に出現するようにしても良いです。このときに、当たり判定が正常にできているかを確認します。

5. 画面外に落下したらゲームオーバー

プレイヤーのY座標が画面外に移動したら、プレイヤーを消滅させるなどしてゲームオーバー表示をします。

6. 一定ラインを超えたらゲームクリア

プレイヤーの移動距離が一定を超えたらゲームクリアとします。上記画面ではゴールオブジェクトに接触したらクリアとしていますが、簡単にできそうな方法で問題ありません。

補足:床オブジェクト出現のアルゴリズムについて

床オブジェクト出現のアルゴリズムについて補足します。
出現タイミングは、床オブジェクトの横幅以上のスクロールが発生したときです。例えば、床オブジェクトの横幅が 32px の場合、前回床オブジェクトが出現した時よりも 32px 画面が動いていたら、出現判定を行うようにします。

また、極端にランダムな高さで出現させると、プレイヤーがジャンプで飛び移れなくなるので注意です。
方法としては、前回出現させた床のY座標を覚えておいて、その値から例えば±128px 以内の位置に出現させる(最大のジャンプ力を考慮します)、とすると無理がない位置に出現させることが可能となります。

それと同じ考え方で、同じ高さに連続して出現させる場合は、前回の高さそのままで出現させるようにします。

ゲームを拡張する

ラン&ジャンプ系ゲームはスマートフォン向けのゲームジャンルとして人気なので、それらを参考に、色々なアクションを追加していくのも楽しいと思います。

例えば、ジャンプ中にタップすると2段ジャンプしたりや長押しで滑空など……。

アクションのバリエーションで、様々なゲーム性が表現できるのが、ラン&ジャンプ系ゲームの魅力ではないかと思います。

ハイパーカジュアルゲームナイト Voodooが語るランアクションゲーム開発Tips - Unityステーション

こちらの動画でラン系ゲームのTipsが紹介されており、より詳しく知りたい方にはおすすめとなります。

ラン系ゲームの定義

  • 1. A地点からB地点への移動:スタート地点Aから目的地Bへ向かう要素があればラン系ゲーム
  • 2. 「動き」こそがゲームプレイ:2Dであればジャンプ。3Dの場合は左右 (画面の奥に向かって移動する)
  • 3. 既存の道のり:進むルートをショートカットできる。ルート分岐の発生など
  • 4. 障害物:障害物への衝突ルールをどのようにするか

1. A地点からB地点への移動

最近のラン系ゲームの傾向は、目的地Bが複数用意されている。それにより同じコースを複数回遊べるようになっている。

2. 「動き」こそがゲームプレイ

キャラクターを動かすための操作方法の種別

  • シンプルな空間ナビゲート:ジャンプ、または左右移動
  • 物理的空間:物理空間(地面)を傾けるなど物理空間を操作するタイプ。ルート選択しやすい
  • アビリティの活用:走るための能力の組み合わせを選択してコースを攻略するタイプ
  • 全自動の動き:環境をコントロールするタイプ
  • ダブル:2通りの使い方がある
  • 描画:フリック操作で図形を書くことで動きが決まる
  • タイピング:文字入力で動きを決める

3. 既存の道のり

目的に到達するまでの道のりをどのように定義するのか?

  • ショートカット:プレイヤーのスキルで時間短縮可能な道のりにする
  • 分岐:プレイヤーのスキルでゴールが変化する

4. 障害物

障害物に衝突したときの挙動をどうするか?

  • フェイル:失敗にする(即死)
  • 進行性あり:ライフを失うだけにする
  • デザイン性の高いもの
  • クイズ:瞬時の判断を試される障害物(○×クイズ。正解を選ぶと先に進める)

関連記事

Godot Engineでのラン&ジャンプゲームの作り方は以下の記事に書いています。

FPSカメラの作り方

フィールドを歩き回るような3Dのゲームを作る際に、主観視点で周りをグルグル見渡せるカメラがあると便利です。ちょうどFPSゲームで使われるカメラです。この記事では、そういったFPSカメラの作り方を説明したいと思います。

作り方概要

FPSカメラを作るには、『「3次元座標系」と「回転座標系」との相互変換』を実現します。これができればFPSカメラは簡単に作れます。

3次元座標系とは、おなじみXYZの座標系です。正確には今回はカメラの操作だけなので、視点座標と注視点座標の操作となります。

そして回転座標系とは、水平方向の回転と垂直方向の回転からなる座標系です(ただ、この用語は正確ではないかもしれません…)。

今回カメラを左右に回したり上下を見れるようにしたいので、水平・垂直方向を基準に回転ができる座標系が必要となるわけです。

ここまでをまとめると、

  1. 3次元座標系から回転座標系へ変換する
  2. プレイヤーの入力を受け取る
  3. 受け取った値を元に、3次元座標系へ逆変換する 

この3つが実装できれば、FPSゲームのようにカメラをグルグル回すことが可能となります。

1.3次元座標系から回転座標系へ変換する

まずは、3次元座標を回転座標に変換します。正確には「カメラ視点」と「カメラ注視点」を元に、「水平方向の回転角度」と「垂直方向の回転角度」を計算します。

水平方向の回転角度

水平方向の回転角度を出すのは簡単です。視点から注視点への方向ベクトルを求め、Atan2を使うことで求めることができます

これはよくSTGを作るときに、狙い撃ちの角度を求めるために使うものですね。コードは以下のようになります。

 

// HAngleの取得関数
float FpsCamera::_GetHAngle(Vec3D* vEye, Vec3D* vTgt)
{
    // 注視点への向きベクトルを求める
    Vec3D vDir;
    Vec3D_Sub( &vDir, vTgt, vEye );
    
    // HAngle(XZ平面での角度)を求める
    float deg = Atan2Deg( -vDir.z, vDir.x );
    
    float __ADJ = 90.0f; // 調整角度(Z方向を基準に回転する)
    deg += __ADJ;
    
    // -180~180に丸める
    if(deg > 180.0f)  { deg -= 360.0f; }
    if(deg < -180.0f) { deg += 360.0f; }
    
    return deg;

注意点としては、90度の角度調整が入っていることです。

なぜ90度なのかというと、(x,y,z)=(0,0,1)を正面とした変換を行いたいためです。よく分からなければ、「とりあえず90度足すもの」ぐらいの理解でもいいと思います。

ちなみにこのコードの関数名として定義した「HAngle」というのは水平方向(Horizon)の角度という意味です。

垂直方向の回転角度

続いて垂直方向の回転角度の計算方法です。ちょっと変わったことをしています。

// VAngleの取得関数
float FpsCamera::_GetVAngle(Vec3D* vEye, Vec3D* vTgt)
{
    // 注視点への向きベクトルを求める
    Vec3D vDir;
    Vec3D_Sub( &vDir, vTgt, vEye );
    
    float fFront;
    {   // カメラの前方方向値
        Vec3D _vFront;
        Vec3D_Copy( &_vFront, &vDir );
        _vFront.y = 0; // XZ平面での距離なのでYはいらない
        fFront = Vec3D_Length( &_vFront );
    }
    
    // Y軸とXZ平面の前方方向との角度を求める
    float deg = Atan2Deg( -vDir.y, fFront );
    
    // 可動範囲は-90~90
    if(deg > 90.0f)  { deg = 180.0f  - deg; }
    if(deg < -90.0f) { deg = -180.0f - deg; }
    
    return deg;

ポイントは2つです。

  1. 可動範囲を持たせる。垂直の回転は-9090の範囲を超えないようにする
  2. fFrontを使うのは横軸がXZ平面における距離のため
XZ-Y平面での回転角度を求める

1についてですが、上下にカメラを回す場合、-90~90を超えないようにしています。90を超えると左右が反転してしまうので、可動範囲を設定しておきます。まあ通常90度を超えて回転することはありえないで、このような制限を入れて問題ないと思います。

2についてですが、横軸に当たるのはXZ平面なので、XZ方向への距離を使用しています。XでもなくZでもなく、XZというのがポイントですね。

2.プレイヤーの入力の反映

先ほどの_GetHAngle()/_GetVAngle()で取得した値に、回転量を足しこむだけです。

// カメラの回転値を更新する
void FpsCamera::UpdateRotate()
{
    // ★1.視点・注視点→回転座標への変換-----------
    Vec3D vEye, vTgt;
    GetEye(&vEye); // カメラ視点の取得
    GetTgt(&vTgt); // カメラ注視点の取得
    m_HAngle = _GetHAngle( &vEye, &vTgt ); // 水平方向の回転角度を取得
    m_VAngle = _GetVAngle( &vEye, &vTgt ); // 垂直方向の回転角度を取得
    
    // ★2.入力を元に回転する-----------------------
    // ■視点移動(回転)
    Vec2D stick; // ※予め移動値が入っているとします

 

Xへの移動(例えば左右キーの入力)を水平方向の回転、Yへの移動(例えば上下キーの入力)を垂直方向の回転として設定しています。

3.回転座標から3次元座標への変換

最後に入力を反映した値を逆変換します。以下、いままでの処理をまとめたコードとなります。

(回転のついでにカメラの平行移動もしています)

// カメラを移動・回転させる
void FpsCamera::Move()
{
    // ★1.視点・注視点→回転座標への変換-----------
    m_HAngle = _GetHAngle(GetEye(), GetTgt());
    m_VAngle = _GetVAngle(GetEye(), GetTgt());
    
    // ★2.入力を元に回転する-----------------------
    // ■視点移動(回転)
    Vec2D stick; // ※予め移動値が入っているとします
    m_HAngle -= stick.x; // 水平方向への回転
    m_VAngle += stick.y; // 垂直方向への回転
    
    // ■平行移動
    float mx, mz; // XZ方向 ※予め移動値が入っているとします
    
    // ・3次元ベクトルにセット
    Vec3D vTranslate;
    Vec3D_Set( &vTranslate, mx, 0, mz );
    
    // ★3.回転座標→3次元座標への変換-------------
    // ■視点座標を取得する
    Vec3D vEye;
    GetEye( &vEye );
    // ■注視点は初期化しておく(後で代入します)
    Vec3D vTgt;
    Vec3D_Zero( &vTgt );
    
    // ■HAngle/VAngleを行列に変換する
    Matrix34 mRot;
    Matrix34_Identity(&mRot);
    Matrix34_RotXYZDeg( &mRot, m_VAngle, m_HAngle, 0 ); // 回転行列生成(※1)
    
    // ・平行移動
    Vec3D_Transform( &vTranslate, &mRot, &vTranslate );
    vTranslate.y = 0.0f; // XZ平面での移動なので、Y移動値は無視する
    Vec3D_Add( &vEye, &vEye, &vTranslate ); // 視点を動かす
    
    // ・回転
    Vec3D vDir;
    Vec3D_Set( &vDir, 0, 0, 1.0f ); // Z方向を基準に回転
    Vec3D_Transform( &vDir, &mRot, &vDir ); // 方向ベクトルの回転(※2)

 

 ポイントは※1の回転行列の生成、※2の方向ベクトルの回転です。

まず※1のところですが、水平方向の回転を「Y軸の回転」、垂直方向の回転を「X軸の回転」と見立てて、回転行列を作っています。

そして、視点から注視点への方向ベクトル(x,y,z)=(0,0,1)に対して、回転行列で回転させています。

ここで_GetHAngle()で+90度していた(Z軸を正面にしている)のが生きてくるわけです

最後に

このコードでは考え方を提示しているだけなので、このままコピーしても動きません。(そもそも環境によってベクトルや行列の関数は異なりますし…)

そこでこのコードを参考にFPSカメラを作る場合のデバッグ方法ですが、

  • まずは水平方向(Hangle)が正しく動くかをデバッグする
  • 続けて垂直方向(VAngle)が正しく動くかをチェックする

という手順で少しずつチェックすることをオススメします。

というのも、水平方向・垂直方向を最初から一緒に動かそうとすると、原因がよく分からずに失敗することが多いです。1つ1つを別の問題として調べていくと問題点も見つけやすいです。(経験談

あと回転させるのであればクォータニオンを使ったほうが早いと思ったかもしれません。ですが、クォータニオンで計算すると、どうやら真後ろを向くときに上を経由してしまうのでうまくいかないようです。

 

Unityで2Dゲームを作るのにおすすめな参考書

今や世界で最も使われている(日本でもダントツ人気)のUnityのおすすめの参考書を紹介します。

なお、主に2Dゲームを作っている開発者としての個人的な視点からのおすすめとなる点にご注意いただければと思います。

入門者向け

Unityの教科書 Unity 2023完全対応版

2Dゲームに限らず、Unity初心者全員におすすめしたい本ですね。とにかく手順が丁寧な説明で図が豊富であるため、多機能で理解が難しいUnityをとてもわかりやすく説明できています。

ただ、題材となるゲーム内容が「ルーレット」「タイミングゲーム」「避けゲーム」「Doodle Jump」「的あてゲーム」といったカジュアル過ぎるゲームであるというのが欠点です。

出典:Unityの教科書 Unity 2021完全対応版 2D&3Dスマートフォンゲーム入門講座

ハイパーカジュアルゲームを作りたいのであればこれでも良いのですが、もう少しゲームっぽいものを作りたい場合には別の本で勉強する必要があります。

ちなみにハイパーカジュアルゲームについてはこのページにまとめています。

初心者〜中級者向け

Unityの寺子屋 定番スマホゲーム開発入門

こちらは初心者には少し難しいかもしれませんが、さきほどの本よりもゲームらしい(「 Incremental game (放置系ゲーム) 」「 Cookie Clicker (クッキークリッカー) 」「 2Dサイドビューの Platformer (プラットフォーマー) 」)ゲームを題材としています。

出典:Unityの寺子屋 定番スマホゲーム開発入門

特に「放置系ゲーム」「クリッカー系ゲーム」はスマートフォン向けのゲームとして人気が高く収益性が高い(継続率が高い)ことで知られていますので、ゲームで生計を立てていきたいと考えている人には良い題材ではないかと思います。

出典:[Unite]個人ゲームアプリ開発者はいかにして生きていくのか? 「和尚」が語るゲーム作家の生き方指南

UnityではじめるC# 基礎編 改訂版

「Unityの寺子屋 定番スマホゲーム開発入門」と同様、いたのくまんぼう氏による Unityの教本です。

こちらの本では「 Escape the room game (脱出ゲーム) 」「 Physics puzzle game (物理パズルゲーム) 」という、これまたスマートフォンで人気のあるゲームジャンルです。

出典:UnityではじめるC# 基礎編

題材のチョイスが絶妙で、こちらもこれからUnityでゲームを作っていきたい人にはおすすめの本です。

楽しく学ぶ Unity2D超入門講座

こちらの本はサイドビューのアクションゲームの作り方を解説している本です。

出典:楽しく学ぶ Unity2D超入門講座

ただ、1冊丸々サイドビューのアクションを作るので、より高度なもの(素材画像がシンプルなので、そうは見えないのではありますが……。ゲームプログラム的にという意味で)を作る方法がわかります。

サイドビューアクションに興味ある人にはおすすめの本となっています。

中級者〜上級者向け

ゲームの作り方 改訂版 Unityで覚える遊びのアルゴリズム

こちらは、ゲームプログラムを上達したいと考えている方にぜひとも読んでもらいたい本です。Amazonのレビューの評価は平均「★3」と決してよくありませんが、個人的にはとても素晴らしい内容の本だと思っています。

理由としては以下のとおりです。

  • 本では部分的にアルゴリズムが紹介されているだけなので、全体の流れを理解するにはソースコードをしっかり読む必要がある
  • 他の本よりも制作難易度の高いゲームで、より高度なプログラム知識が必要になる
  • 古いバージョンのUnityで作られていてそのままでは動かないので、置き換え作業が必要になってプログラムやUnityのバージョンによる違いについて考える力が身につく
  • ゲームを面白くするための Tips が細かく描かれていて勉強になる

ゲームプログラムの技術力を上げるには、他人のソースコードを読んで理解し、自分のゲームに応用できるのが一番力が付きます。また題材も「ドットイートゲーム」「パズルゲーム」など、高いプログラム能力が求められるものとなります。それにより、単に簡単なサンプルゲームをコピーするだけの状態から抜け出し、ゲームプログラムのための本当の力が身につくためとなります。

出典:ゲームの作り方 Unityで覚える遊びのアルゴリズム

Unityのバージョンがやや古いため、そのままでは動作しないところがありますが、それもUnityを理解するための過程であると考えると勉強になります。ゲームアルゴリズムそのものはいつの時代でも不変なので、アルゴリズムを勉強するためであれば環境の違いはほぼ気にならないはずです。

誘導レーザーのアルゴリズムを細かく説明されています

(出典:ゲームの作り方 Unityで覚える遊びのアルゴリズム

なお、Kindle版は固定レイアウトで読みにくいので、中古の単行本で良い(その方が安い)と思います。

あと改定前のバージョンだと、紙の本であれば数百円で買えてしまうので、古いバージョンでも気にならない人はこちらもおすすめです。

こんな良書が110円……?!(※配送料は数百円かかります)

以下に中古本のリンクを貼っておきます。

ちなみにサンプルがダウンロードできない、という低評価レビューがついていたのですが、今はサポートページから普通にダウンロードできるようです。