株式会社グローバルゲート公式ブログ

Unityで2Dシューティングに挑戦

こんにちは、株式会社グローバルゲートのモーリーです。 
 
当社は8月13日(水)から8月17日(日)まで夏季休業日となります。 
ご迷惑をおかけしますがよろしくお願いいたします。 



さて、今回はUnityを使ってゲームの作成に挑戦してみたいと思います。 

Unityとは?

Unityは、Unity Technologies社が開発・提供しているゲームエンジンです。 
2005年にMac向けゲーム開発ツールとして誕生して以来、アップデートを重ね、現在ではPC、スマホ、Web、SwitchやPlay Stationなどのコンシューマー、Apple Vision ProのようなAR/VR環境など多種多様なプラットフォームに対応しています。 
 
「ゲームエンジン」としてリリースされましたが、CADデータを3D化したり自動車用アプリケーションのインターフェイスとして使用されるなど、その活用範囲はゲームだけに留まりません。 
 
開発者にとっても、C#という比較的学習しやすいプログラミング言語を使いながら、3Dや2Dのコンテンツを専用のエディタを使って直感的に作成できる点も大きな強みです。 

Unityによって作られたゲームの例

Unityで制作されたゲームは数多くあって数え切れませんが、有名どころでは次のようなタイトルがあります。遊んだことのあるゲームもあるのではないでしょうか。

Among Us

ポケモンGO

原神 *

Shadowverse

アイドルマスターシリーズ

Fall Guys

*中国のUnityは独自のカスタマイズが加えられているため、一般的に利用できるUnityとは別物と考えたほうがいいかもしれません。

Unityは公式ドキュメントが非常に充実しているだけでなく、Udemyをはじめとしたオンライン講座や、初心者向けから上級者向けまで幅広い書籍が豊富に揃っています。そのため、個人や小規模チームでも学習・実践しやすい環境が整っており、実際に少人数で開発されたにもかかわらず世界的なヒットを記録した作品も数多く誕生しています。

小規模チームや個人によって作られたゲームの例

動物タワーバトル

スイカゲーム

溶鉄のマルフーシャ

Unityで2Dシューティングゲームを作る

さて、それではUnityを使ってゲームづくりに挑戦してみましょう。 
今回は2Dシューティングゲームを最低限動くところまでやってみたいと思います。

ちなみに:あらすじ 
彼女は小さな魔女のお届け屋さん。 
今日もお気に入りのほうきに乗り、大切な荷物を届けるために青空を駆け回っています。 
 
しかし最近、空ではイタズラ好きのカラスたちが配達を邪魔しようと狙っています。 
あなたは次々と迫るカラスたちをかわし、ときには魔法を使い、大切な荷物を無事に届けなければなりません。 
 
あなたの魔法で迫りくるカラスたちを撃退し、荷物を安全に届けましょう! 
小さな魔女の空の冒険がいま幕を開けます。 

……どこかで聞いたことのあるストーリーですが、リリース予定はありませんのでご容赦ください……。

Unityのインストール

Unityのホームページよりインストーラーをダウンロードし、実行してインストールします。

Unity Hubというプロジェクト管理ツールを使用してプロジェクトの作成やUnityのバージョン管理などを行い、このUnity Hub経由でUnityエディターを起動します。 
 
そのためインストール後はまずはじめにUnity Hubが起動します。

プロジェクトの作成 

New Projectをクリックし、新規プロジェクトを作成します。

テンプレートとなるプロジェクトを選択します。 
今回は2Dシューティングゲームを作りたいのでUniversal 2Dを選択します。 

これがUnityのエディターです。この画面上にオブジェクトを配置し、各種処理を追加してゲームとして成立するように作っていきます。

自機の追加

それでは、シューティングゲームを作っていきましょう。

まずは自機を追加します。 
Assetsタブに画像をドラッグしてプロジェクトに画像を登録し、そのAssetsからSceneにドラッグします。 

自機用の画像はChatGPTで
ほうきに乗った魔女の少女・黒いワンピース・黒髪・ショートカット・赤いリボン・ドット絵
として制作してもらいました。

どこかで見たことがありますが気にせず進めます。

自機を操作できるようにする 

画像を配置しただけでは何もできませんので、この自機を矢印キーで上下左右に移動できるようにします。 

Inspectorタブ内にあるAdd Componentをクリックし、Player Inputを選択します。

Player Inputコンポーネントを追加した段階でActionsではInputSystem_Actionsを選択されていると思いますが、何も選択されていない場合をクリックして選択してください。

デフォルトで作成されているInputSystem_Actionsはキーボードやコントローラーの基本的な操作が定義されています。
ほとんどの場合このまま使用して問題ないでしょう。

この段階でキー操作を受け取ることができるようになりましたが、その受け取った操作によってどのような処理を行うかを定義しないと何も起きません。
 上を押したときは上に移動、右を押せば右に移動…といった処理はスクリプトによって行いますので、そのスクリプトを作成します。

Assetsタブの中で右クリックし、Create→Scripting→Empty C# Scriptを選択します。 

これで#アイコンのファイルが生成されました。名前部分をクリックして「PlayerController」に名前を変更しておきます。 
(スクリプトファイルは今後いくつか作成する必要があるので、名前をきちんと付けておかないと混乱の元になります) 

このPlayerControllerファイルをダブルクリックし、エディタ(私はVSCodeがインストールされているのでVSCodeが開きます)で編集します。 
次のようにスクリプトを書きます。

単に移動させるだけでは画面外に移動できてしまったりゲームらしい気持ちのいい操作感になりませんので、画面外にはみ出さない制御と動きに慣性をつけてほうきの浮遊感を再現しました。

using UnityEngine; 
using UnityEngine.InputSystem; 
 
public class PlayerController : MonoBehaviour 

    private Vector2 moveInput; 
    private Vector2 currentVelocity; 
 
    [SerializeField] private float acceleration = 10f; 
    [SerializeField] private float deceleration = 8f; 
    [SerializeField] private float maxSpeed = 5f; 
 
    [Header("Bounds Settings")] 
    [SerializeField] private float padding = 0.5f;        // 画面端との余白 
 
    private Vector2 minBounds; 
    private Vector2 maxBounds; 
 
    void Start() 
    { 
        // カメラのビューポート(0〜1)をワールド座標に変換 
        Camera cam = Camera.main; 
        minBounds = cam.ViewportToWorldPoint(new Vector3(0, 0, cam.nearClipPlane)); 
        maxBounds = cam.ViewportToWorldPoint(new Vector3(1, 1, cam.nearClipPlane)); 
    } 
 
    public void OnMove(InputValue val) 
    { 
        moveInput = val.Get<Vector2>(); 
    } 
 
    private void Update() 
    { 
        Vector2 targetVelocity = moveInput * maxSpeed; 
 
        float lerpFactor = (moveInput.magnitude > 0.01f) ? acceleration : deceleration; 
        currentVelocity = Vector2.Lerp(currentVelocity, targetVelocity, lerpFactor * Time.deltaTime); 
 
        transform.Translate(currentVelocity * Time.deltaTime); 
 
        // === 移動後の位置を画面内に制限 === 
        Vector3 clampedPos = transform.position; 
        clampedPos.x = Mathf.Clamp(clampedPos.x, minBounds.x + padding, maxBounds.x - padding); 
        clampedPos.y = Mathf.Clamp(clampedPos.y, minBounds.y + padding, maxBounds.y - padding); 
        transform.position = clampedPos; 
 
    } 
}

スクリプトの記述が終わったら保存し、Assetsタブ内のPlayerContollerファイルをInspectorにドラッグします。これでオブジェクトとスクリプトファイルが関連付けられました(Unityでは「アタッチする」と呼ぶ) 

この時点で動かしてみると(上段の▶️アイコンをクリック)、実際にキーボードの矢印キーで操作できるようになっています。

実際に操作ができるようになると面白くなってきますね!

敵キャラの追加 

シューティングゲームでは欠かせない敵キャラも実装してみます。 
 
敵のオブジェクトは自機とは異なり、同一のオブジェクトが複製されていくつも画面に表示されます。 
そのため、Prefabという雛形的なオブジェクトとして登録して使用します。

まずは敵キャラ用の画像ファイルをAssetsにドラッグしてUnityから使用できるようにします。
カラスのドット絵をChatGPTで制作しました。

この敵キャラを動かすためのスクリプトを作成します。
自機のスクリプトと同様にCreate → Scripting → Empty C# Script を選択し、ファイル名をEnemyとしておきます。

今回は簡単に右から左に移動するだけの動きにしてみます。
次のようにスクリプトを書き、保存します。

using UnityEngine; 
 
public class Enemy : MonoBehaviour 

    [SerializeField] private float speed = 3f; 
 
    void Update() 
    { 
        transform.Translate(Vector3.left * speed * Time.deltaTime); 
 
        // 左に完全に出たら削除 
        if (transform.position.x < Camera.main.ViewportToWorldPoint(new Vector3(0, 0, 0)).x - 1f) 
        { 
            Destroy(gameObject); 
        } 
    } 
}

Assetsのカラスを一旦Hierarchyに追加し、Hierarchy上でそのカラスを選択して上記のEnemyスクリプトをアタッチします。
Scene上にカラスが表示されますが、後ほどPrefab化して消去しますので一旦このままで進めます。

スクリプトがアタッチされたカラスをPrefab化します。
Assets内にフォルダを作成し、そのフォルダ内にドラッグすることでそのオブジェクトはPrefabとして扱われます。

敵キャラの出現はスクリプトで行いますので、Hierarchyからは削除しておきます。
Prefab化したオブジェクトはHierarchyから削除してもPrefabフォルダ内には残ります。

このPrefab化したカラスが画面上に出現するようにします。
上下位置はランダムで複数出現するようなスクリプトを作成し、そのスクリプトをダミーのオブジェクトにアタッチします。

Hierarchy上で右クリックし、Create Emptyを選択します。
名称をEnemySpawnerとします。

Assets上で右クリックしてCreate → Scripting → Empty C# Scriptを選択し、名称をEnemySpawnerとします。
そしてダブルクリックして次のスクリプトを記述します。

using UnityEngine; 
 
public class EnemySpawner : MonoBehaviour 

    [SerializeField] private GameObject enemyPrefab; 
    [SerializeField] private float spawnInterval = 2f; 
    [SerializeField] private float minY = -3f; 
    [SerializeField] private float maxY = 3f; 
 
    private float timer; 
 
    void Update() 
    { 
        timer += Time.deltaTime; 
        if (timer >= spawnInterval) 
        { 
            SpawnEnemy(); 
            timer = 0f; 
        } 
    } 
 
    private void SpawnEnemy() 
    { 
        float y = Random.Range(minY, maxY); 
        float x = Camera.main.ViewportToWorldPoint(new Vector3(1.1f, 0, 0)).x; // 画面右外 
        Vector3 spawnPos = new Vector3(x, y, 0); 
 
        Instantiate(enemyPrefab, spawnPos, Quaternion.identity); 
    } 
}

このスクリプトは出現させる敵のPrefabを選択する必要があります。
InspectorのEnemy PrefabでPrefab化したカラスを選択します。

ランダムにカラスが出現するようになりました。
ただ当たり判定やダメージの設定をしていないため、カラスが魔女を素通りしてしまいます。
カラスと接触時にダメージを受けるようにしましょう。

敵と自機の接触時にダメージを受けるようにする 

まずはカラスに当たり判定を設定します。 
prefabフォルダ内のカラスを選択し、Add ComponentでBox Collider 2Dを選択します。 

is Triggerにチェックを入れます。
is Triggerはオブジェクトが接触するときにイベントは発生しますが物理演算(跳ね返ったり止まったり)は行いません。
今回敵キャラは接触してもそのまま通過するようにしたいのでこのチェックを入れます。

次にこのカラスが敵キャラかどうかの判定をするために、タグ付けを行います。
 InspectorのTagAdd Tagを選択し、+をクリックしてEnemyと入力し、Saveをクリックします。

Tagの選択でEnemyが選べるようになっていますので選択してください。

自機にはBox Collider 2DRigidBody 2Dを追加します。
Box Collider 2Dはis Triggerにチェックを入れ、Rigidbody 2DはBody TypeKinematicSimulatedにチェックを入れてください。

自機と敵キャラの接触時にはどういうアクションを起こすかについてはゲームによりけりですが、今回は

・泣き顔のグラフィックに差し替わる
・一定時間操作不能になる
・点滅する


という処理にしたいと思います。

自機の操作用に作成したPlayerContollerスクリプトを修正します。

using UnityEngine; 
using UnityEngine.InputSystem; 
using System.Collections; 
 
public class PlayerController : MonoBehaviour 

    private Vector2 moveInput; 
    private Vector2 currentVelocity; 
 
    [SerializeField] private float acceleration = 10f; 
    [SerializeField] private float deceleration = 8f; 
    [SerializeField] private float maxSpeed = 5f; 
 
 
 
    [Header("Bounds Settings")] 
    [SerializeField] private float padding = 0.5f;        // 画面端との余白 
 
    [Header("Damage Settings")] 
    [SerializeField] private Sprite normalSprite; 
    [SerializeField] private Sprite damagedSprite; 
    [SerializeField] private float disableDuration = 2f; 
    [SerializeField] private float blinkInterval = 0.2f; 
 
    private Vector2 minBounds; 
    private Vector2 maxBounds; 
    private SpriteRenderer spriteRenderer; 
 
    private bool isDisabled = false; 
    private float disableTimer = 0f; 
    private Coroutine blinkCoroutine; 
 
    void Start() 
    { 
        Camera cam = Camera.main; 
        minBounds = cam.ViewportToWorldPoint(new Vector3(0, 0, cam.nearClipPlane)); 
        maxBounds = cam.ViewportToWorldPoint(new Vector3(1, 1, cam.nearClipPlane)); 
 
        spriteRenderer = GetComponent<SpriteRenderer>(); 
        spriteRenderer.sprite = normalSprite; 
    } 
 
    public void OnMove(InputValue val) 
    { 
        if (!isDisabled) 
        { 
            moveInput = val.Get<Vector2>(); 
        } 
    } 
 
    private void Update() 
    { 
        if (isDisabled) 
        { 
            disableTimer -= Time.deltaTime; 
            if (disableTimer <= 0f) 
            { 
                isDisabled = false; 
                spriteRenderer.sprite = normalSprite; 
                if (blinkCoroutine != null) StopCoroutine(blinkCoroutine); 
                spriteRenderer.enabled = true; // 表示を戻す 
                moveInput = Vector2.zero;         // 入力リセット 
                currentVelocity = Vector2.zero;   // 速度リセット 
             
                RefreshMoveInput(); 
            } 
            return; // 操作無効 
        } 
 
        Vector2 targetVelocity = moveInput * maxSpeed; 
        float lerpFactor = (moveInput.magnitude > 0.01f) ? acceleration : deceleration; 
        currentVelocity = Vector2.Lerp(currentVelocity, targetVelocity, lerpFactor * Time.deltaTime); 
        transform.Translate(currentVelocity * Time.deltaTime); 
 
        Vector3 clampedPos = transform.position; 
        clampedPos.x = Mathf.Clamp(clampedPos.x, minBounds.x + padding, maxBounds.x - padding); 
        clampedPos.y = Mathf.Clamp(clampedPos.y, minBounds.y + padding, maxBounds.y - padding); 
        transform.position = clampedPos; 
 
 
    } 
 
    private void RefreshMoveInput() 
    { 
        Vector2 input = Vector2.zero; 
        if (Keyboard.current.upArrowKey.isPressed) input.y += 1; 
        if (Keyboard.current.downArrowKey.isPressed) input.y -= 1; 
        if (Keyboard.current.rightArrowKey.isPressed) input.x += 1; 
        if (Keyboard.current.leftArrowKey.isPressed) input.x -= 1; 
 
        moveInput = input.normalized; 
    } 
 
    private void OnTriggerEnter2D(Collider2D other) 
    { 
        if (other.CompareTag("Enemy") && !isDisabled) 
        { 
            isDisabled = true; 
            disableTimer = disableDuration; 
            spriteRenderer.sprite = damagedSprite; 
 
            if (blinkCoroutine != null) StopCoroutine(blinkCoroutine); 
            blinkCoroutine = StartCoroutine(BlinkDuringDisabled()); 
        } 
    } 
 
    private IEnumerator BlinkDuringDisabled() 
    { 
        while (isDisabled) 
        { 
            spriteRenderer.enabled = false; 
            yield return new WaitForSeconds(blinkInterval); 
            spriteRenderer.enabled = true; 
            yield return new WaitForSeconds(blinkInterval); 
        } 
    } 

スクリプトを修正するとInspectorにDamage Settings欄が追加されます。

ここでNormal Spriteで現在の自機のイメージ、Damged Spriteで無き顔のイメージを選択します。

泣き顔もChatGPTにお任せしました。
キ◯はこんな顔で泣いたりはしないと思いますが

カラスに接触すると泣き顔になり、一定時間操作不能になる処理が実現しました!
少しはゲームらしくなってきたんじゃないでしょうか。

攻撃(弾の発射)ができるようにする

◯キは攻撃魔法を使うことはありませんでしたが、とはいえさすがに逃げ回るだけのシューティングでは面白くありません。

スペースキーを押したら魔法の弾丸を放ち、カラスと接触すると弾とカラスも消えるという処理を追加します。

魔法の弾は敵キャラと同様にPrefab化し、画面上に複数出現するようにします。
グラフィックはChatGPTで制作しました。

カラスを作成したときと同様に弾のPNG画像をAssetsにドラッグし、さらにHierarchyにドラッグします。

この弾にアタッチするスクリプトを作成します。
Assets内で右クリックしてCreate → Scripting → Empty C# Script を選択し、名前をBulletにします。

直線的に自機から右に弾が飛んでいくようにします。
Bulletをダブルクリックで開いて次のスクリプトを記述します。

using UnityEngine; 
 
public class Bullet : MonoBehaviour 

    [SerializeField] private float speed = 10f; 
 
    void Update() 
    { 
        // 右方向に移動 
        transform.Translate(Vector3.right * speed * Time.deltaTime); 
    } 

このスクリプトを弾のオブジェクトにアタッチし、その弾をPrefabフォルダにドラッグします。
Prefab化が終わったらHierarchyから弾のオブジェクトは削除しておきます。

魔法を発射する位置を指定するために空のオブジェクトを自機の子要素として配置します。
Hierarchyの+をクリックし、Create Emptyを選択します。 

生成されたオブジェクトはドラッグして自機の子の位置に配置します。

Sceneの十字矢印をクリックすることでオブジェクトをドラッグで移動させることができます。
弾を発射させたい位置に調整します。
今回はほうきの柄の先端から発射するようにしました。

スペースキーを押したらPrefab化した弾丸オブジェクトが指定位置から発射されるようにします。
PlayterControllerスクリプトに次の処理を追加します。

using UnityEngine; 
using UnityEngine.InputSystem; 
using System.Collections; 
 
public class PlayerController : MonoBehaviour 

    private Vector2 moveInput; 
    private Vector2 currentVelocity; 
 
    [SerializeField] private float acceleration = 10f; 
    [SerializeField] private float deceleration = 8f; 
    [SerializeField] private float maxSpeed = 5f; 
 
 
 
    [Header("Bounds Settings")] 
    [SerializeField] private float padding = 0.5f;        // 画面端との余白 
 
    [Header("Damage Settings")] 
    [SerializeField] private Sprite normalSprite; 
    [SerializeField] private Sprite damagedSprite; 
    [SerializeField] private float disableDuration = 2f; 
    [SerializeField] private float blinkInterval = 0.2f; 
 
    //弾用 
    [Header("Bullet Settings")] 
    [SerializeField] private GameObject bulletPrefab;    // 弾のプレハブ 
    [SerializeField] private Transform firePoint;        // 弾の発射位置 
 
 
    private Vector2 minBounds; 
    private Vector2 maxBounds; 
    private SpriteRenderer spriteRenderer; 
 
    private bool isDisabled = false; 
    private float disableTimer = 0f; 
    private Coroutine blinkCoroutine; 
 
    void Start() 
    { 
        Camera cam = Camera.main; 
        minBounds = cam.ViewportToWorldPoint(new Vector3(0, 0, cam.nearClipPlane)); 
        maxBounds = cam.ViewportToWorldPoint(new Vector3(1, 1, cam.nearClipPlane)); 
 
        spriteRenderer = GetComponent<SpriteRenderer>(); 
        spriteRenderer.sprite = normalSprite; 
    } 
 
    public void OnMove(InputValue val) 
    { 
        if (!isDisabled) 
        { 
            moveInput = val.Get<Vector2>(); 
        } 
    } 
 
    private void Update() 
    { 
        if (isDisabled) 
        { 
            disableTimer -= Time.deltaTime; 
            if (disableTimer <= 0f) 
            { 
                isDisabled = false; 
                spriteRenderer.sprite = normalSprite; 
                if (blinkCoroutine != null) StopCoroutine(blinkCoroutine); 
                spriteRenderer.enabled = true; // 表示を戻す 
                moveInput = Vector2.zero;         // 入力リセット 
                currentVelocity = Vector2.zero;   // 速度リセット 
             
                RefreshMoveInput(); 
            } 
            return; // 操作無効 
        } 
 
        Vector2 targetVelocity = moveInput * maxSpeed; 
        float lerpFactor = (moveInput.magnitude > 0.01f) ? acceleration : deceleration; 
        currentVelocity = Vector2.Lerp(currentVelocity, targetVelocity, lerpFactor * Time.deltaTime); 
        transform.Translate(currentVelocity * Time.deltaTime); 
 
        Vector3 clampedPos = transform.position; 
        clampedPos.x = Mathf.Clamp(clampedPos.x, minBounds.x + padding, maxBounds.x - padding); 
        clampedPos.y = Mathf.Clamp(clampedPos.y, minBounds.y + padding, maxBounds.y - padding); 
        transform.position = clampedPos; 
 
        //弾用 
        if (Keyboard.current.spaceKey.wasPressedThisFrame) 
        { 
            Fire(); 
        } 
 
 
    } 
 
    //弾用 
    private void Fire() 
    { 
        Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); 
    } 
 
 
    private void RefreshMoveInput() 
    { 
        Vector2 input = Vector2.zero; 
        if (Keyboard.current.upArrowKey.isPressed) input.y += 1; 
        if (Keyboard.current.downArrowKey.isPressed) input.y -= 1; 
        if (Keyboard.current.rightArrowKey.isPressed) input.x += 1; 
        if (Keyboard.current.leftArrowKey.isPressed) input.x -= 1; 
 
        moveInput = input.normalized; 
    } 
 
    private void OnTriggerEnter2D(Collider2D other) 
    { 
        if (other.CompareTag("Enemy") && !isDisabled) 
        { 
            isDisabled = true; 
            disableTimer = disableDuration; 
            spriteRenderer.sprite = damagedSprite; 
 
            if (blinkCoroutine != null) StopCoroutine(blinkCoroutine); 
            blinkCoroutine = StartCoroutine(BlinkDuringDisabled()); 
        } 
    } 
 
    private IEnumerator BlinkDuringDisabled() 
    { 
        while (isDisabled) 
        { 
            spriteRenderer.enabled = false; 
            yield return new WaitForSeconds(blinkInterval); 
            spriteRenderer.enabled = true; 
            yield return new WaitForSeconds(blinkInterval); 
        } 
    } 

InspectorのPlayer Controller(Script)にBullet Settingsという項目が追加されています。
 Bullet PrefabにPrefabフォルダ内の弾のグラフィック、Fire Pointに先ほど作成した発射位置用のオブジェクトを選択します。

これでスペースキーで魔法の弾丸が発射できるようになりました。
ですがこの弾に当たり判定がないため、カラスを素通りしてしまいます。
弾とカラスが接触したらどちらも消える処理を追加します。

Bulletスクリプトを次のように修正します。Enemyというタグを持つオブジェクトと接触したらそのオブジェクトと自身が消えるという処理を追加します。

using UnityEngine; 
 
public class Bullet : MonoBehaviour 

    [SerializeField] private float speed = 10f; 
 
    void Update() 
    { 
        transform.position += Vector3.right * speed * Time.deltaTime; 
    } 
 
    private void OnTriggerEnter2D(Collider2D other) 
    { 
        if (other.CompareTag("Enemy")) 
        { 
            // 弾と敵の両方を削除 
            Destroy(other.gameObject);    // 敵を削除 
            Destroy(gameObject);          // 弾を削除 
        } 
    } 

Prefab内の弾丸オブジェクトにBox Collider 2Dを追加し、is Triggerにチェックを入れます。
続けてRigidBody 2Dを追加し、Body TypeKinematicSimulatedにチェックを入れます。
(自機の設定と同様です)

これできちんと敵を倒せる魔法の弾になりました!

Web用のビルドをアップロードしましたので試してみてください。

まとめ

ということで、今回はUnityで2Dシューティングゲームのごくごく基本の動きを作ってみました。
ここまででもかなり苦労しましたので、ゲームを作るというのは大変だな…と思い知らされました。

 Unityは3Dゲームの開発も可能ですので、機会があればそちらも試してみたいと思います。

【関連記事】

ご相談・お問い合わせ

当社サービスについてのお問い合わせは下記までご連絡下さい。

お電話でのお問い合わせ

06-6121-7581 / 03-6415-8161