役演亭 -Yakuentei- Roleplay with your own characters.
  1. ホーム
  2. 技術記事
  3. ゲーム開発

Unity でプログラミングのみ (C# Script のみ) でゲームを作る方法

公開日 (Published) : 更新日 (Modified) :

プログラマーの方向けに、Unity で極力デザイナに頼らずゲームを作る方法を紹介します。

本動画のミニゲームを例に、C# Script のみ (=マウスによる手作業を極力削減) での実装方法を紹介していきます。

2023年5月30日にリリースされた Unity 2022 LTS にも対応しています!

目次

まえがき (対象読者など)

Unity の入門記事は数多いですが、「既にプログラミングができる方」向けの情報が少ないと言えます。

例えば、Unity Editor の GUI 上であらかじめマウス等をもちいてキャラクターなどを「手動で」配置してから、最低限のスクリプトのみ書く、という「お手軽」スタンスの記事が多いため、「どうやって C# Script のみで実現するか」の情報がなかなか見つかりません。

本記事は、そんな「お手軽」情報ばかりでうんざりしているプログラマー同志の方向けです。

既に何らかの言語 (C# 以外も含む) でプログラミング経験のある方が、これから Unity を始めるにあたり、極力 C# Script のみでゲームを組めるようになるための基礎となるエッセンス (特に「画像」「音」などを C# Script のみで制御する方法) を 1 記事に集約しています。

C# 未経験者や初学者の方でも、他の言語からの類推でコードが理解できるよう、なるべく平易な文法の (例えば LINQ やラムダ式などは使わない) コードにしていますが、C# の基礎レベルの文法は理解できる前提で説明しています。

事前準備 - Unity 環境 (Unity Hub, Unity Editor)

Unity (Hub および Editor) のインストール方法については、Web 上の情報も多いですし、プログラミング経験者の方なら問題なく導入できると思いますので、割愛します。

本記事の内容は、以下のバージョンの Unity Editor で動作確認済です。

  • Unity 2022 LTS (2022.3.0f1)
  • Unity 2021 LTS (2021.3.18f1)

なお、本記事では (一部を除いて) 基本的な API のみ使用しているため、バージョンが異なっても問題が無いことが多いと思われます。

バージョンは特に指定はしませんが、後述する LTS 版を推奨します。

LTS 版の Unity について

Unity Hub から Unity 本体 (Unity Editor) をインストールする際は、「LTS」が付いているバージョンがおすすめです。

LTS とは、Long Term Support (=長期的なサポート) のことで、最新の機能が使えない代わりに安定性を重視したバージョンです。

概ね、2 週間に 1 回くらいの頻度でバグ修正等のマイナーバージョンアップが行われています。(一番下のリビジョン番号が 1 個ずつ上がっていきます)

Unity Hub からインストールできる LTS 版の表示例

不安定でも良いから最新の Unity の機能を試してみたいという方は、「LTS」が付いていない最新バージョンを選択しても良いかもしれませんが、トラブルが起きたときの原因の切り分けが大変ですから、Unity に慣れてから手を出すのが無難でしょう。

事前準備 - 使用素材

本記事では、以下の zip ファイルの中にある素材 (画像ファイル、音声ファイル) を使用しますので、あらかじめダウンロードして下さい。

ダウンロード後、zip ファイルの中に音声 (mp3) ファイルが 5 つ、画像 (png) ファイルが 2 つ格納されていることをご確認下さい。

Resources.zip の中身

使用素材の著作権情報など

本 zip ファイル中の素材は、以下の無料素材サイトで公開されているものを、本記事で扱いやすいように抜粋・加工し、1 つにまとめたものとなります。それぞれの素材サイトに記載の利用規約にしたがって取り扱っていただきますよう、よろしくお願いいたします。

Unity プロジェクトの新規作成

Unity Hub の「プロジェクトメニュー」にて「新しいプロジェクト」をクリックします。

本記事で紹介する例は 2D のゲームですので、テンプレートは「2D」を選択します。

プロジェクト名、保存場所は任意 (ただし日本語のフォルダ名、ファイル名は筆者が Unity の不具合に遭遇したことがあるため、避けたほうが無難) です。筆者は、プロジェクト名 ScriptOnlyTest、保存場所 C:\UnityProj にて動作確認しました。

新規 2D プロジェクトを作成

C# Script の追加と Main Camera への割当

今回の例では、Assets フォルダの直下に C# Script を新規に追加します。(一般的には、Assets フォルダの中であれば直下でなくても構いません)

以下のスクリーンショットのように、Assets フォルダを右クリック → Create → C# Script で追加します。

C# Script の新規作成

ファイル名の入力が求められますが、今回は初期値の NewBehaviourScript.cs のままとします。(一般的には好みの名前にして OK ですが、クラス名も変わるので注意)

C# Script のファイル名入力

C# Script の Main Camera への割当

プロジェクトに C# Script を追加しただけでは、Unity 本体 (Unity Player) は何も実行してくれません。C# Script のみでゲームを作る場合、通常は C# Script をドラッグ & ドロップして Main Camera に割り当てることが必要です。

C# Script の Main Camera への割当

正しく割り当てできた場合、Hierarchy タブで Main Camera をクリックした際、Inspector タブ側に表示される Main Camera のプロパティの最下部に、新規作成した C# Script が追加されます。

Main Camera に正しく C# Script が割り当てできたかの確認

補足:C# Script の適切な割当先について

Main Camera 以外のゲームオブジェクトを自前で追加して、そこに C# Script を割り当てる方法でも動きますが、ユーザーから見える画面を描画する Main Camera に割り当てるのが最も簡単かつ無難 (C# Script のみでゲームプログラムを書いたときに不具合が起きにくい) です。

特に、将来 Unity の GUI コンポーネント (ボタンやテキストボックスなど) を使用したいと考えている方は、ボタンやテキストボックスが表示されるカメラに C# Script が割り当てられていない場合、うまくイベント (クリックなど) が拾えずに正しく動作しないことがあります。

ちなみに、本来の Unity の使い方 (Unity Editor のデザイナ機能も活用) としては、他の GUI ベースの開発環境 (例えば Visual Studio の WinForms や WPF など) と同様、ゲーム画面に配置した各オブジェクトに対して、そのオブジェクトのみに関係する C# コードをイベントドリブン方式で追加していくのが正統なやり方です。

C# Script が Unity から呼ばれることの確認

追加した C# Script が Unity から正しく呼ばれるかどうかの確認のため、デバッグ用のコードを追加します。

コードを編集するため、先ほど追加した C# Script を右クリックし、Open C# Project をクリックします。これにより、通常は Unity と一緒にインストールされた Visual Studio (もしくは Unity が自動的に認識したインストール済の Visual Studio など) が立ち上がるはずです。

Open C# Project

以下のように、Unity によって自動的に生成されたコードを含む C# Script が出てくれば成功です。(Visual Studio のみ立ち上がった場合は、明示的に NewBehaviourScript.cs をダブルクリックして開きます)

Start() メソッドは初期化時に 1 回だけ、Update() メソッドはフレームの更新毎 (例えば 60 FPS のゲームなら 1/60 秒 = 約 0.017 秒毎) に呼び出されるメソッドです。

Start() メソッドと Update() メソッド

動作確認のために追加するコード

以下のデバッグ出力用のコードを、それぞれのメソッドに追加します。(デバッグ出力の様子がリアルタイムで確認できるよう、フレームレートを 5 FPS = 0.2 秒毎に設定しています)

  • Start() メソッド側
Debug.Log("Start() Called.");
Application.targetFrameRate = 5;
  • Update() メソッド側
Debug.Log("Update() Called. Time.deltaTime = " +
    Time.deltaTime + " [sec]");

使用している各 API の概要は以下のとおりです。詳細な仕様は、Google 検索等で出てくる Unity 公式のドキュメントなどでご確認下さい。

  • Debug.Log() メソッド
    • 指定した文字列を、Unity Editor の Console 上などにデバッグ出力します。
  • Application.targetFrameRate プロパティ
    • 目標フレームレートを FPS (フレーム毎秒) 単位で指定します。
    • -1 (初期値) を指定すると無制限 (PC の性能に依存) になります。
    • ここで指定したフレームレートが達成できるよう、Unity が Update() メソッドの呼び出し間隔を調整してくれます。
  • Time.deltaTime プロパティ
    • 前回の Update() からの実際の経過時間を、float 型の「秒」単位で取得できます。
    • 特にフレームレート可変のゲームを作りたい場合は、この値を参照してアニメーションの速度などを調整します。
    • 処理落ち等で実際の経過時間が長めになってしまった場合の対策にも使用できます。

コード追加後の Visual Studio のスクリーンショットです。

コード追加後の Start() メソッドと Update() メソッド

動作確認の実施

うまくいけば、上記で追加したデバッグ出力は、Console タブ側に表示されますので、実行前にあらかじめ Console タブに切り替えておきます。

Console タブへの切り替え

ここまで出来たら、Unity Editor の画面上部中央にある Play ボタン (再生マーク) を押して実行を開始します。(ショートカットキーは Ctrl + P です)

Play ボタン (再生マーク)

うまくできていれば、Console タブ側に約 0.2 秒間隔で以下のようなメッセージが表示されるはずです。(正確に 0.2 秒とは限らず、若干ばらつきます)

Console タブへの出力確認 (約 0.2 秒間隔)

もう一度 Play ボタン (再生マーク) を押すまでは、ずっと実行しっぱなしになりますので、Console 側にはずっとメッセージが出力され続けます。

実際にもう一度 Play ボタンを押下して実行を停止してみましょう。これで、一番最初に出ているはずの Start() メソッド側のデバッグ出力が確認できるようになるはずです。(要:上方向へのスクロール)

停止後の Start() メソッド出力の確認

画像・音声ファイルを C# Script から使用する

いよいよ本記事の「本題」かつ「目玉」です。

Unity 以外の環境でゲームプログラムを書いたことのある方なら、本節 (と次節の解説内容) を理解できれば、本記事だけでは足りない情報を Unity の公式ドキュメントや Web 上の情報などで補うことで、自力でゲームを作れるようになるはずです。

画像・音声ファイルの Unity プロジェクトへの組み込み

本記事では、最も簡単に使用できる Resources.Load() API をもちいて、画像・音声ファイルの読み込みを行います。

  • 欠点として、リソースの動的な確保・解放ができないため、メモリ使用量の大きなゲームでは別記事で言及しているアセットバンドルや、Addressable.LoadAssetAsync() 等の細かなリソース管理ができる API を利用した方が良いでしょう。

Resources.Load() を使用する場合は、必ず、Assets フォルダの「直下」に Resources という名前でフォルダを作成し、その中に画像・音声等の素材 (アセット) ファイルを置く必要があります。(Resources フォルダの中であればサブフォルダを作って管理することは可能)

事前準備 - 使用素材」の節でダウンロードした Resource.zip の中身を、以下のスクリーンショットを参考に Assets\Resources の直下に配置されるように置いてみましょう。(エクスプローラー側で配置しても構いませんし、Unity Editor 側でインポートしても構いません)

Resources.zip の中身の Unity プロジェクトへの配置

Unity Editor 側で素材が認識されると、Unity プロジェクト側のフォルダの中には、各素材ファイルと同名の .meta ファイルが生成されます。これらのファイルには、Unity Editor 側で各素材に対して GUI で行った追加設定 (圧縮方法、読み込みタイミングなど) が保存されます。

.meta ファイルの生成

実際に画像・音声ファイルを読み込んで使用する

説明は動かした後でしますので、まずは以下のコードを丸ごと NewBehaviourScript.cs に貼り付け (上書き) てみましょう。差分は強調表示しています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    // 音楽・効果音関係のメンバー変数
    AudioClip _clipSE;
    AudioSource _audioSrc;

    // 画像関係のメンバー変数
    SpriteRenderer _renderCharacter;
    int _characterX = 0;

    // Start is called before the first frame update
    void Start()
    {
        // 画面サイズの設定 (画面拡大前のオリジナルの高さピクセル数の半分)
        Camera.main.orthographicSize = 90;

        // 音楽・効果音関係の初期化 (ついでに音楽のループ再生も開始)
        AudioClip clipBGM = Resources.Load<AudioClip>("maou_bgm_cyber37");
        _clipSE = Resources.Load<AudioClip>("maou_se_battle_gun01");
        _audioSrc = new GameObject("音声再生器").AddComponent<AudioSource>();
        _audioSrc.loop = true;
        _audioSrc.clip = clipBGM;
        _audioSrc.Play();

        // 画像関係の初期化 (画面拡大前のオリジナル画面サイズ = 320x180 ピクセル)
        Texture2D textureWallPaper = Resources.Load<Texture2D>("pipo_battlebg001");
        Texture2D textureSprites = Resources.Load<Texture2D>("pipo_sprites");
        Sprite spriteWallPaper = Sprite.Create(textureWallPaper,
            new Rect(0, 0, 320, 180), new Vector2(0.5f, 0.5f), 1);
        Sprite spriteCharacter = Sprite.Create(textureSprites,
            new Rect(64, 32, 32, 32), new Vector2(0.5f, 0.5f), 1);
        SpriteRenderer renderWallPaper =
            new GameObject("壁紙").AddComponent<SpriteRenderer>();
        _renderCharacter =
            new GameObject("キャラ").AddComponent<SpriteRenderer>();
        renderWallPaper.sprite = spriteWallPaper;
        _renderCharacter.sprite = spriteCharacter;
        renderWallPaper.sortingOrder = 0;
        _renderCharacter.sortingOrder = 1;
    }

    // Update is called once per frame
    void Update()
    {
        // スペースキーが押されるたびに、キャラを動かし、効果音を鳴らす。
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _characterX += 16;
            _renderCharacter.transform.position = new Vector3(_characterX, 0);
            _audioSrc.PlayOneShot(_clipSE);
        }
    }
}

上記を貼り付けたら、実際に Play ボタン (再生マーク) を押して実行してみましょう。以下のような画面が表示され、音楽も流れるはずです。

スペースキーを押すと、効果音とともに、画面中央のキャラクターが右に移動します。

画像・音声ファイルの読み込みの確認

画像・音声ファイルを使用する処理の解説

説明の順番は多少前後します (必ずしもコードの上から順番とは限りません) が、機能の種類毎に分けて解説していきます。

画面サイズの設定 (orthographicSize)

Unity で制作したゲームは、ユーザーの環境 (ウィンドウモードならウィンドウサイズ、フルスクリーンなら画面の解像度) に合わせて画面サイズを Unity 側が自動的に調整 (拡大・縮小など) してくれます。

しかし、ゲームを作る側の立場としては、オリジナルの画面サイズ (上記の拡大・縮小が行われる前の画面サイズ) を管理できなければ、ゲームで使用する画像のサイズ (ピクセル数など) を決めることができません。

そこで、そのオリジナルの画面サイズを設定するためのプロパティが orthographicSize で、カメラに対して設定します。

Unity の Main Camera は、Camera クラスの main プロパティで取得できるため、実際にアクセスするときは Camera.main.orthographicSize のように記述します。

Camera.main.orthographicSize = 90;

orthographicSize には、オリジナル (拡大・縮小前) の画面サイズの縦方向の半分の値を、ピクセル単位 (正確には後で説明する pixelsPerUnit に依存) で指定します。イメージとしては、以下の画像のような感じです。

orthographicSize の意味

本記事の例では、オリジナルの画面サイズが 320x180 ピクセルですので、orthographicSize には 180 / 2 = 90 を設定しています。

横方向のサイズについては、Unity への設定内容次第で、アスペクト比を保持したまま拡大 (=トリミング有り) したり、アスペクト比を無視して拡大 (=トリミング無し) したり等を設定できるようになっているため、基本的には縦方向のサイズ情報のみが基準となります。

これらの画面サイズの調整の挙動も C# Script から各種 API を叩くことで細かくプログラミングが可能ですが、入門レベル以上の内容になってしまいますので、本記事では割愛します。

画像・音声ファイルからの読み込み (Resources.Load())

以下の 2 箇所が該当します。

// 音楽・効果音関係の初期化 (ついでに音楽のループ再生も開始)
AudioClip clipBGM = Resources.Load<AudioClip>("maou_bgm_cyber37");
_clipSE = Resources.Load<AudioClip>("maou_se_battle_gun01");
// 画像関係の初期化 (画面拡大前のオリジナル画面サイズ = 320x180 ピクセル)
Texture2D textureWallPaper = Resources.Load<Texture2D>("pipo_battlebg001");
Texture2D textureSprites = Resources.Load<Texture2D>("pipo_sprites");

<型名> という表記は、C# のジェネリックという文法で、そのメソッドで扱うデータ型 (Resources.Load() の場合は「戻り値」の型) を指定する書き方です。

  • 音声ファイル (mp3, ogg など) を読み込む場合は AudioClip 型を指定します。
  • 画像ファイル (png, jpg など) を読み込む場合は Texture2D 型を指定します。

また、Resources.Load() では、読み込みたいファイル名を「拡張子無し」で指定します。

なお、_clipSE をローカル変数ではなくメンバー変数にしている理由は、Update() メソッド側からもアクセスしたいからです。

変数名の前に付けているアンダーバー (_) は、単にメンバー変数であることを分かりやすくするためのもの (参考:マイクロソフト社が C# で採用しているコーディングルール) です。

音楽と効果音を再生するための準備

音楽と効果音は、いずれも AudioSource というオブジェクトを使用して再生を行います。

AudioSource から音声を再生するためには、現在動いているシーン (Scene) に対してゲームオブジェクト (GameObject) に格納したうえで登録することが必要です。

_audioSrc = new GameObject("音声再生器").AddComponent<AudioSource>();

ゲームオブジェクトとは、Unity の「用語」かつ「オブジェクトの型名」のことで、Unity のシーンにぶら下がるすべてのオブジェクトの「入れ物」です。つまり、ゲーム内で音声や画像を「表舞台に出す」際には、すべて GameObject の中に「格納」する必要があります。

逆を言うと、今後実行する処理のために、今は使っていない音声や画像を裏舞台に置いておくだけの場合は、GameObject として実体化させる必要はありません。

GameObject の例

なので、まずは「入れ物」であるゲームオブジェクトを作るために new GameObject("オブジェクトの名前") という記述が必要になります。オブジェクトの名前は、Unity Editor 上などでプログラマーが管理するためのものなので、プログラマーの好みで命名して構いません。

そして、新たに作られたゲームオブジェクトの AddComponent() メソッドを呼ぶことで、<型名> で指定されたオブジェクトを Unity 側で用意してくれます。(おそらく Unity の内部で new と最低限の初期化処理をやっているものと思われます)

(注:GameObject そのものは、AddComponent() した後は参照する機会がほとんどないため、本記事のコード例では読み捨てています)

音楽の再生

音楽 (いわゆる BGM) の再生は、AudioSource に対していくつかのプロパティの設定を行ってから Play() メソッドを呼び出すことで行います。

_audioSrc.loop = true;
_audioSrc.clip = clipBGM;
_audioSrc.Play();

読んで字のごとくかと思いますが、上記の 3 行のとおり、ループ再生を ON (true) にしたうえで、clipBGM に格納されている音声データを再生しています。

つまり、mp3 や ogg ファイルから読み込んだ AudioClip オブジェクトを AudioSource オブジェクトに渡してあげることで BGM として再生されることを意味します。

効果音の再生

Unity では、音楽 (BGM) と効果音 (SE) の再生を、1 つの AudioSource オブジェクト (インスタンス) で行うことができます。

  • 前述の Play() メソッドは、バックグラウンドでの再生、つまり BGM の再生に適したメソッドです。
  • これに対し、効果音 (SE) のように単発で再生するのに適したメソッドが PlayOneShot() です。

PlayOneShot() メソッドでは、単発再生したい AudioClip オブジェクトをメソッドの引数に指定します。

_audioSrc.PlayOneShot(_clipSE);

ただし、もし BGM と SE で再生の条件を変えたい場合 (例えば音量バランスを BGM と SE で変えたいなど) は、音楽用と効果音用で AudioSource のインスタンスを分ける必要があります。(複数の AudioSource オブジェクトが GameObject としてシーン内にあっても、Unity 側でミキシングして再生してくれます)

参考までに、AudioSource オブジェクトの音量を設定するためには、volume プロパティを使用します。(例えば _audioSrc.volume = 0.5f とすれば 50% の音量になります)

画像を描画するための準備

画像に関しても、実際に描画するためには GameObject に格納する点では音声側と変わりませんが、間に挟まるオブジェクトの種類が 1 つ増えるので、ちょっと複雑です。

  • Texture2D オブジェクト
    • 画像ファイルなどから読み込んだ画像データ「全体」を示します。
  • Sprite オブジェクト
    • 画像データから、その一部の範囲 (キャラクターなど) を切り出したものです。
    • より正確には、画像データの実体は Texture2D が持っており、Sprite 側は (切り出し位置情報などをもとに) 参照のみを行います。
  • SpriteRenderer オブジェクト
    • 実際に画面に対して Sprite オブジェクトの描画を行うオブジェクトです。

絵で表すと、以下のような関係図になります。

各画像系オブジェクトの関係図

流れとしては、まず Texture2D から部分画像を Sprite として切り出します。
(詳細は後回しにします)

Sprite spriteWallPaper = Sprite.Create(textureWallPaper,
    new Rect(0, 0, 320, 180), new Vector2(0.5f, 0.5f), 1);
Sprite spriteCharacter = Sprite.Create(textureSprites,
    new Rect(64, 32, 32, 32), new Vector2(0.5f, 0.5f), 1);

そして、実際の画面描画を行うために、SpriteRenderer オブジェクトを GameObject に格納して登録します。

SpriteRenderer renderWallPaper =
    new GameObject("壁紙").AddComponent<SpriteRenderer>();
_renderCharacter =
    new GameObject("キャラ").AddComponent<SpriteRenderer>();

最後に、生成した SpriteRenderer に対して、描画する Sprite と、描画方法を指定します。

renderWallPaper.sprite = spriteWallPaper;
_renderCharacter.sprite = spriteCharacter;
renderWallPaper.sortingOrder = 0;
_renderCharacter.sortingOrder = 1;

Texture2D から Sprite を切り出す個所の詳細

Sprite.Create() メソッドは、指定した Texture2D から Sprite を切り出すためのメソッドです。

  • 第 1 引数に、切り出し元の Texture2D オブジェクトを指定します。
  • 第 2 引数に、切り出し領域 (x, y, 幅, 高さ) を画像の「左下」基準で指定します。(ピクセル単位)
  • 第 3 引数に、ピボットの位置 (=Sprite 描画時の座標指定の基準 x, y 位置) を、Sprite 領域の「左下」基準で指定します。(0~1 の相対位置)
    • Unity の標準的な挙動としては、ピボットが画像の中央に置かれることが多いので、X 方向、Y 方向ともに 0.5 (=中央) とした座標管理に慣れておくことをおすすめします。
  • 第 4 引数に、pixelsPerUnit (Unity 内部で 1 と指定したときのピクセル数) を指定します。
    • 筆者としては pixelsPerUnit = 1 にするのが最も分かりやすいと考えています。(Unity 上で 1 と書けば必ず 1 ピクセル単位になるので)

キャラクター側の処理 (下記) を例に、絵をもちいて説明します。

Sprite spriteCharacter = Sprite.Create(textureSprites,
    new Rect(64, 32, 32, 32), new Vector2(0.5f, 0.5f), 1);

Unity は、3DCG 分野における「左手座標系」を採用したゲームエンジンです。

したがって、画像の原点が「左上」ではなく「左下」にあることに注意が必要です。

特に Unity 以外の環境で 2D ゲームのプログラミング経験のある方は、下記の図の赤いエリアの画像を切り出すときに、ついつい (x = 64, y = 0, 幅 = 32, 高さ = 32) とやってしまいがちですが、Unity の座標系では (x = 64, y = 32, 幅 = 32, 高さ = 32) となりますので、間違いの無いよう注意して下さい。

Sprite 生成時の座標基準位置の具体例

ピボットについては、実際に SpriteRenderer をとおして描画する際の基準位置になるため、詳細は後回しにします。

(ここも、2D ゲームのプログラミングに慣れている方は注意が必要なところで、一般的な 2D 用のプログラミング環境だとピボットの位置も「左上」にあることが普通なので、慣れるまでは大変だと思います)

画面に Sprite を描画する SpriteRenderer の初期化処理

以下の処理は、AudioSource をゲームオブジェクトに入れて登録する方法と同じですので、説明は割愛します。(これを行わないと、Sprite を実際に画面上に描画することができません)

SpriteRenderer renderWallPaper =
    new GameObject("壁紙").AddComponent<SpriteRenderer>();
_renderCharacter =
    new GameObject("キャラ").AddComponent<SpriteRenderer>();

登録後に、Sprite (=画像そのもの) と SpriteRenderer (=画像描画器) を結びつける必要がありますが、ここも AudioClip (=音声そのもの) と AudioSource (=音声再生器) の関係と同じです。

renderWallPaper.sprite = spriteWallPaper;
_renderCharacter.sprite = spriteCharacter;

注意が必要なのは、z 座標の概念のない 2D ゲームの場合、sortingOrder (=描画順序) の指定を忘れてはいけないことです。

renderWallPaper.sortingOrder = 0;
_renderCharacter.sortingOrder = 1;

実は、Unity は必ずしもオブジェクトを生成した順番どおりに描画してくれるとは限りません。

今回の場合は背景を先に描画しておく必要がありますから、必ず背景画像側 (renderWallPaper) の sortingOrder の方が小さな値になるように設定しておきましょう。

(0 と 1 以外の組み合わせでも、例えば 100 と 200 のような組み合わせでも OK です)

SpriteRenderer の描画位置の指定

実際にキャラクターが動いた方が座標指定への理解が深まりやすいので、キーボードからの入力 (スペースキー) でキャラクターが動くようにしています。

キー入力の監視や描画座標の更新は、常時行う必要があるため、Update() メソッド側に入れている点もポイントです。(背景画像は動かす必要が無いので Start() 側のみです)

// Update is called once per frame
void Update()
{
    // スペースキーが押されるたびに、キャラを動かし、効果音を鳴らす。
    if (Input.GetKeyDown(KeyCode.Space))
    {
        _characterX += 16;
        _renderCharacter.transform.position = new Vector3(_characterX, 0);
        _audioSrc.PlayOneShot(_clipSE);
    }
}

先ほど、「画像の原点は左下にある」と説明しましたが、画面 (カメラ) への描画時の原点は、以下のように「中央」になりますので、注意して下さい。(筆者も Unity を使い始めたばかりの頃は混乱しました…)

画面描画時の座標指定

上記の画像にも説明を入れましたが、このタイミング (描画時) で Sprite.Create() 時に指定したピボットの位置が使用されます。

_characterX の初期値は 0 にしていますので、初期状態では完全に中央に表示されるはずですが、例えば spriteCharacter 側のピボットの位置を (0.5f, 0.5f) から (0, 0) に変えると、右上方向に 16 ドットずつずれて描画されるはずですので、「ピボットの意味がいまいち理解できないんだけど…」という方は色々と試してみると良いでしょう。

ドットを荒く拡大描画する Pixel Perfect Camera の有効化

ここまでの手順を実際にやってみて、「ゲーム画面がぼやけているんだけど?」と感じた方もいるはずです。

実は、Unity 標準のカメラの挙動だと、ゲーム画面を拡大する際に、なめらかに拡大してしまうため、レトロゲームのようにドットを目立たせたい (=解像度の低いゲーム画面を荒く拡大表示したい) 場合はかえって不都合が生じます。

おそらく Unity 公式側もその問題は認識していたのでしょうか、Unity 2021 から「Pixel Perfect Camera」という画面を荒く拡大するための機能が追加されました。

解像度の高いゲームを作る場合には不必要な機能ですが、本記事で紹介しているゲームのようにオリジナル画面の解像度が非常に荒いゲーム (320x180 ピクセル) では、Pixel Perfect Camera を使用することで、より「レトロゲームっぽさ」を演出することができます。

Pixel Perfect Camera を C# Script のみで有効化する

公式ブログに GUI (デザイナ) 経由で Pixel Perfect Camera を使用する方法が書かれていますが、本記事では趣旨どおりに C# Script のみで有効化してみたいと思います。

スクリプトから有効化するためには、まず、以下の 2 つの using 宣言を追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.U2D;
using Unity.VisualScripting;

そして、Start() メソッドで orthographicSize の設定を行っている個所の下に、以下の Pixel Perfect Camera を有効化するための一連の処理を追加します。

// 画面サイズの設定 (画面拡大前のオリジナルの高さピクセル数の半分)
Camera.main.orthographicSize = 90;

// Pixel Perfect Camera の設定
// (Unity 2021 から追加された、レトロゲームのように画面を荒く拡大する機能)
PixelPerfectCamera pxPerfCam = Camera.main.AddComponent<PixelPerfectCamera>();
pxPerfCam.assetsPPU = 1;
pxPerfCam.refResolutionX = 320;
pxPerfCam.refResolutionY = 180;
pxPerfCam.upscaleRT = true;
pxPerfCam.cropFrameX = true;
pxPerfCam.cropFrameY = true;

各プロパティの概要は以下のとおりです。
(最低限の設定しか行っていませんので、上述の公式ブログの説明も参考にしてみて下さい)

  • assetsPPU プロパティ
    • PPU = Pixels Per Unit の値を指定します。
    • Sprite.Create() 時に指定した pixelsPerUnit と同じ値を設定する必要があります。(本記事の場合は 1 に統一しています)
  • refResolutionX, refResolutionY プロパティ
    • オリジナルの画面解像度を設定します。
    • 本記事では 320x180 ピクセルですので、それぞれ 320 と 180 を指定します。
  • upscaleRT プロパティ
    • 描画対象となる SpriteRenderer を、拡大後の荒いゲーム画面の各ドットに合わせて描画するか否かの設定です。
    • 通常は true を設定します。(false だとなめらかになる代わりにレトロゲームっぽさが薄くなります)
  • cropFrameX, cropFrameY プロパティ
    • ゲーム画面の拡大倍率を整数倍に限定し、かつ余白を黒色で埋めるするための設定です。
    • 例えばモニタの解像度が 1024x768 だった場合、1024 / 320 = 3.2, 768 / 180 = 4.27 ですので、整数倍に限定した場合の拡大率は 3 倍になります。(4 倍だとモニタからはみ出てしまいます)
    • このケースのとき、cropFrameX = true, cropFrameY = true とすることで、拡大率がちょうど 3 倍 (960x540) となり、残りの余白は黒色で埋められます。

コードを追加したら、実際に動作確認してみましょう。

Unity Editor 上で、ゲーム画面の端あたりの境界線をドラッグ & ドロップすることで画面サイズを調整できますので、オリジナル画面の拡大が整数倍単位で行われているかどうかを確認してみると良いでしょう。

Pixel Perfect Camera の動作確認

実際にゲームとして動くようにプログラミングする

ゲームプログラミング経験のある方なら、ここまで説明した内容をもとに、(足りない情報を Unity 公式ドキュメントなどで補うことで) 十分ゲームを作ることができるかと思います。

なぜなら、ゲームを作るうえで必要な「キー入力」「画面出力」「画像」「音声」「経過時間」などを扱うための最低限の API (=道具) が揃ったからです。

しかし、ここで説明を止めてしまうのでは「投げやり」ですので、実際のプログラミング例をもとに、Unity の API をもちいてゲームを組み立てるためのテクニックを紹介していきます。

実際にゲーム化を行った C# Script

本記事上に直接貼り付けると長くなりすぎる (581 行) ため、別ファイルとして、以下のリンクからダウンロードできるようにしました。

今まで作業してきたプロジェクトの NewBehaviourScript.cs に上書きできるように作ってありますので、上記のファイルの内容でコードを上書きしたうえで、Play ボタン (再生マーク) を押して実行してみて下さい。

ゲームオーバー画面

操作説明をしていませんでしたので、おそらくほとんどの方はすぐにゲームオーバー (上記) になってしまったかもしれませんが、操作説明は以下のとおりです。

  • ゲームオーバー時の操作
    • スペースキーを押すと、スコアをリセットしてゲームを再開
  • プレイ中の操作
    • スペースキーを押すと、弾を発射。
      (ただし、画面左下のオレンジ色の数字が 100 のときのみ発射可能)
    • カーソルキー上下で、自機を移動。

ゲームの基本的なルールなどは、以下のとおりです。

  • 画面右側から登場するオバケを、ひたすら避けるか、弾を発射して倒すかを「エンドレス」で繰り返すシンプルなシューティングゲームです。
  • 体力 (ライフ) の概念はなく、オバケと衝突したら即ゲームオーバーです。
  • 生き残っている時間が長いほど、画面右下のスコア (青色表示) が増えていきます。
  • 弾を打ってオバケを倒すと 100 点獲得できますが、一度弾を打つと次弾の装填までに 100 カウント (5 秒) かかります。
  • なので、基本は「避けゲー」としてプレイする必要があり、どうしても避けきれないときだけ弾を打つことが、ハイスコア獲得のためのカギです。

コード内容の解説 (重要な箇所のみ)

ソースコードにはコメントを多めに入れましたので、基本的には上から順番に読んでいけば理解できるようにしてあります。(C# の文法上も、なるべく簡単な基礎レベルのものだけを使って書いています)

本節では、重要な箇所に絞って、特記事項を説明していきます。

Unity とは直接関係しない、ゲームプログラミングそのもののテクニックの解説については割愛いたします。

定数定義

マジックナンバー (数字の直打ち) を避けるための定数を、コードの最初の方でまとめて定義しています。

ソースコードを読んで理解する際は、まずは定数定義の部分はコメントだけを流し読みして、実際のコードの中で参照している箇所が出てきたら適宜確認すると良いでしょう。

メンバー変数定義

定数定義に引き続き、メンバー変数もまとめて定義しています。

すべてのメンバー変数には、既説のとおりアンダーバー (_) をつけて容易に区別できるようにしています。

特にプログラミング経験が豊富な方には言うまでもないことかもしれませんが、メンバー変数やグローバル変数は、同じスコープの中にあるすべての関数やメソッドの動作に影響を及ぼすものであるため、ローカル変数などとは容易に区別できるようにしておくことが、プログラミングミスによる事故、バグを回避するために重要です。

ゲーム画面の初期化

ここで初出の QualitySettings.vSyncCount プロパティが出てきますが、これは垂直同期 (V-Sync) の有無や、その挙動を制御するためのプロパティです。

注意点として、Unity Editor 上で Play (再生ボタン) を押してゲームを実行した場合は、vSyncCount プロパティに設定した値が完全に「無視」されます。(exe 等の実行可能ファイル形式でビルドした本番用オブジェクトでのみ有効)

Unity Editor で Play する際の垂直同期の有無を設定したい場合は、以下の場所にあるドロップダウンボックスの中にある VSync (Game view only) チェックボックスで行います。

Unity Editor 上の V-Sync ON/OFF 設定

スプライトの初期化

これまでのプログラミング例では、単に 1 枚の飛空艇の画像だけをゲーム画面に出していましたが、本ゲームプログラムではアニメーションさせるため、全パターンのスプライト画像を読み込んでいます。

C# Script からアニメーションさせる方法は簡単で、単に SpriteRenderer の sprite プロパティを、時間が経過する毎に順番に差し替えるようにコードを書けば良いだけです。

Sprite 自体はすでにメモリ上に読み込まれているので、内部処理が重くなることを心配する必要はありません。

スプライト描画オブジェクトの初期化

本ゲームで必要になる最大枚数の SpriteRenderer を、あらかじめ全部生成しています。例えば、オバケの 1 画面内の最大出現数は 4 匹なので、初期化の時点で 4 枚すべての SpriteRenderer をオバケ用に確保しています。

Unity の GameObject の生成・解放処理は、比較的「重たい」と言われていますので、シューティングゲームのようにリアルタイム性が要求されるゲームではあらかじめ必要枚数分を全部生成しておいた方が、処理落ちが起こりにくくなります。

UpdateTimeCount()

本ゲームプログラムでは、フレームレートが変動しても問題なく動くように、実際のフレーム毎の経過時間をもとにキャラの移動量や、コマ割りアニメーションの時間間隔などをまじめに計算・反映させています。

もしフレームレートが固定 (例えば 60 FPS 固定) のゲームを作るのであれば、Time.deltaTime の値を参照せずに、Update() メソッドが常に 1/60 秒毎に呼ばれている前提で「手抜き実装」するのも 1 つの選択肢です。

しかし、特に 3D ゲームのように動作が重いゲームを作りたいのであれば、低スペック PC だと設計通りのフレームレートが出ない (=Update() メソッドが呼ばれる時間間隔が長くなる) ことも十分に考えられますので、ユーザー (プレイヤー) の方に配慮するのであれば、まじめに Time.deltaTime による実時間ベースのゲームプログラミングを行ったほうが良いでしょう。

UpdateCharacterMoveEnemy()

本ゲームでは、敵 (オバケ) の出現にランダム性を持たせるため、乱数を発生させています。

Unity には、int 型および float 型の乱数を発生させるためのメソッド UnityEngine.Random.Range() が組み込まれているため、その機能を呼び出しています。

おわりに

本記事では、Unity で C# Script のみをもちいてゲームプログラミングするために必要な最低限かつ重要な機能 (主に画像、音声関係など) を紹介しました。

実際に一般公開するゲームを作るにあたっては、これだけではまだ物足りないと思います。

例えば、フォントファイル (ttf 形式など) をもちいたテキストの描画方法については、本記事中で紹介すると長くなりすぎるため、別記事としました。

他にも足りないと思われるものはいろいろありますが (例えば 3D ゲーム関連)、おいおい別記事として拡充していく予定です。

しかし、本記事の内容で C# Script でのゲームプログラミングの基本は押さえられるはずなので、足りない情報を (Unity の公式ドキュメントや、その他 Web 上の記事などで) 補うための足掛かりになると思います。

筆者である私「鈴木YE」としましては、本記事が、そのような足掛かりとして、これから Unity を始めようとしているプログラミング経験者の方々の「スタートアップ」に役に立っていただけることを願っております。

筆者「鈴木YE」が制作したゲームの紹介

本記事の筆者「鈴木YE」は、「役演亭」名義でインディーゲームを制作し、Steam, DLsite でダウンロード販売を行っています。

公開中のゲームのスクリーンショット

実際に、本記事で紹介したように C# Script メインでインディーゲームを作っていますので、Unity に興味のあるゲームプログラマーの方にも参考になると思います。

実際に公開中のゲームを Unity で動かしているスクリーンショット

もし本記事が役に立ち、何かお礼がしたいと考えている方は、役演亭の作品を購入してレビューなどを書いたり、友人・知人に紹介していただけると大変うれしいです。

また、購入するまで行かなくても、体験版 (無償) も公開していますので、ブログ等で体験版のレビュー記事などを書いて紹介していただけるだけでもうれしいです。

今後も、インディーゲームの制作を継続しつつ、技術情報の記事も拡充していく予定ですので、役演亭 / 鈴木YE をよろしくお願いいたします。

主な更新履歴

「ゲーム開発」一覧

Unity でプログラミングのみ (C# Script のみ) でゲームを作る方法
(現在開いているページです)
Unity の文字列描画 (TextMesh Pro) を C# Script のみで行う方法
Unity で極力デザイナに頼らず TextMesh Pro で文字列を描画する方法を紹介します。
Unity の C# Script で「無」から音を産み出す方法
音声ファイル無し、スクリプトのみでゼロから音を作って鳴らす方法を紹介します。
Unity アセット暗号化 実例付き入門
AssetStudio 等のツールで素材をぶっこ抜かれないように保護する方法の基本説明です。
Unity アセットバンドル (AssetBundle) 実例付き入門
外部のアセットファイルを読み込み可能にする「アセットバンドル」機能の説明です。
Unity の Time.deltaTime で測った時間がおかしい? (float 型に注意)
Unity で Time.deltaTime をもちいて時間計測を行う際の注意点を解説します。
【暫定】RPG Maker Unite のアドオンからゲーム動作に介入する方法
従来ツクールのように、既存ゲーム動作に介入するための手法を調べた暫定記事です。
【IL2CPP対応】RPG Maker Unite のアドオンからメソッド差替する方法
iOS など IL2CPP でのビルドが必要な場合でも使えるメソッド差替方法を紹介します。