見出し画像

はじめてゲームを作ったので、そのはなし

こんにちは。以前から興味があって独学で学んだゲーム製作ですが、オリジナルでゲームを作ってみたいと思い、「RocketRacer」というゲームの製作を行いました。


ロゴも作ってみた

拙い出来の物ではありますが、自分なりにこだわったり考え抜いたところを共有したくnoteを書きました。公開して誰でもプレイ可能にしているので、このnoteを読まずとも遊んでいただけたら幸いです。

ゲーム概要と操作、プレイ先のURL

宇宙空間でロケットを操作して、向かってくる障害物となる岩を避けながらゴールまでのタイムを競うゲームです。
PC、スマホどちらでもプレイ可能です。

操作方法
PC 矢印左:左へ移動 矢印右:右へ移動
スマホ 画面左タップ:左へ移動 画面右タップ:右へ移動

画面左右端に行くと逆側に移動する、パックマンとかマリオ3の上に登っていく土管ステージみたいなシステムを導入しています。

1プレイ1分前後でできると思います。

プレイ先URL: https://kindness136.github.io/RocketRacer/

製作期間:およそ1週間
イラスト、プログラム:コップ
音声:効果音ラボより
ロゴ製作:canvaより

製作の話

今回のゲーム製作でやりたかったことは以下の通りです。

・unityの本で勉強したことを活かす
・製作が難しすぎない、簡単なゲームを作ってみる
・画面左右端から逆側に出てくるシステムを作る

unityの勉強にはこちらの本を使いました。

勝手がわからずとりあえずで買った本でしたが、何か別でオススメがあれば教えていただきたいです。

ひと通りこの本の内容を終わらせ、得た知識やプログラムを活かしてオリジナルのゲームを作ってみたく今回挑戦してみました。

スクリプトを描くよ~

ゲームに使うロケットや岩のデザインで気に入るものがないな~と思い、思い切って自分で描くことにしました。

少し昔のソフトのドット絵描画ソフト「edge」を使ってポチポチ描きました。ロケット、岩だけじゃなくてゴールバーや背景もこれで作りました。

結果として少々チープな見た目になったし、なによりクソ大変でした。絵描くのそんな得意じゃないので……

からあげ?

プログラムを書こ~~

力”りき”を入れたプログラミング部分についてです。イラストよりこっちが本命。

コードのなかで特に力を入れたプレイヤーの操作に関わるプログラムとカメラ移動に関わるプログラムについて話したいと思います。

少々専門的な内容だし、学びが浅いのでわかりにくい部分があると思いますが何卒……

ロケット操作について

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;

public class PlayerController : MonoBehaviour
{
    Rigidbody2D rigid2D;
    GameObject countdownText;
    GameObject timerText;

    float upForce = 0.75f; // 上昇速度
    float maxUpSpeed = 7.5f; // 最高上昇速度
    float sideForce = 2.5f; // 左右移動速度
    Vector3 worldDir;

    //開始時のカウントダウン
    float countdown = 4f;
    int count;
    float time = 0;

    float characterWidth = 1f; // 自機の幅
    float screenWidth = 4.7f; // 画面幅

    // Start is called before the first frame update
    void Start()
    {
        Application.targetFrameRate = 60;
        this.rigid2D = GetComponent<Rigidbody2D>();
        this.countdownText = GameObject.Find("CountDown");
        this.timerText = GameObject.Find("Time");
    }

    // Update is called once per frame
    void Update()
    {
        //開始時カウントダウン
        if (countdown >= 1)
        {
            countdown -= Time.deltaTime;
            count = (int)countdown;
            this.countdownText.GetComponent<TextMeshProUGUI>().text = this.count.ToString("");
        }

        //カウントダウン後ゲームスタート
        if (countdown <= 1)
        {
            Destroy(this.countdownText); //カウントダウンテキストの消去

            //スマホ向けの操作、タップ位置の取得
            if (Input.GetMouseButtonDown(0))
            {
                worldDir = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            }

            //画面から指を離したら座標を0へ
            else if (Input.GetMouseButtonUp(0))
            {
                worldDir = new Vector3(0, 0, 0);
            }

            this.time += Time.deltaTime; //経過時間
            this.timerText.GetComponent<TextMeshProUGUI>().text = this.time.ToString("F1"); //UIに時間を表示

            float speedy = Mathf.Abs(this.rigid2D.velocity.y); //プレイヤの上方向速度

            if (speedy < maxUpSpeed) //上方向スピード制限
            {
                this.rigid2D.AddForce(transform.up * this.upForce);
            }

            // 左右移動
            int key = 0;
            if (Input.GetKey(KeyCode.RightArrow) || worldDir.x > 0) key = 1; //矢印右、または画面右側タップで右へ
            if (Input.GetKey(KeyCode.LeftArrow) || worldDir.x < 0) key = -1; //矢印左、または画面左側タップで左へ

            this.rigid2D.AddForce(transform.right * key * this.sideForce);

            //以下chatGPT参考
            Vector2 currentPosition = this.transform.position; //現在の位置

            // 左端に着いたら右端へ
            if (currentPosition.x < -screenWidth / 2 - characterWidth / 2)
            {
                float newPositionX = screenWidth / 2 + characterWidth / 2;
                this.transform.position = new Vector2(newPositionX, currentPosition.y);
            }

            // 右端に着いたら左端へ
            else if (currentPosition.x > screenWidth / 2 + characterWidth / 2)
            {
                float newPositionX = -screenWidth / 2 - characterWidth / 2;
                this.transform.position = new Vector2(newPositionX, currentPosition.y);
            }
        }
    }

    void OnTriggerEnter2D(Collider2D other) //ゴールバーに触れたとき
    {
        float goaltime = Mathf.Floor(this.time * 10.0f) / 10.0f; //ゴールタイムの小数第2位以下の切り捨て
        Debug.Log(goaltime);
        
        PlayerPrefs.SetFloat("GoalTime", goaltime); //ゴールタイムの設定

        GetComponent<AudioSource>().Play(); //ゴール時に歓声

        //ゴール後0.9秒でクリアシーン遷移
        IEnumerator DelayedAction()
        {
            yield return new WaitForSeconds(0.9f);
            SceneManager.LoadScene("ClearScene");
        }
        StartCoroutine(DelayedAction());
    }
}

なげ~!移動だけじゃなくて開始時のカウントダウンだったりタイマーもplayerControllerに持たせてるためちょっと長くなってしまいました。

Update関数以降をかいつまんでお話します。

~カウントダウン~

開始時カウントダウンのプログラムをplayerControllerに持たせることでカウントダウン中のロケットの操作を制御しています。

カウントダウン中の制御はプレイヤー側にしか与えていません。本来は岩の生成プログラムにも与えるべきではあるのですが、プレイヤーだけに与えてプレイしてみたらぜんぜんいい感じだったのでこのままにしました。

~上方向の移動~

とにかく無条件に上方向へ力をかけ続けることであらかじめ決めておいた最高速度までスピードが上昇していきます。

これにより岩がぶつかってきたとき(上から下方向の力)に減速がかかる、というのが容易に設計できます。ベクトルの基礎やね。

裏を返すと岩にぶつからないことで速度が維持されより良いタイムが狙えるようになるということです。

余談になりますが、うまく岩を避けれるかというとそれは難しく、岩の出方はx座標について完全ランダムで警告など一切ありません。ノーダメ?クリアはむちゃむずかしです。

~画面両端の逆から出てくるやつ~

さて、先述の「画面左右端から逆側に出てくるシステム」についてですが、どんな仕組みかわからなかったしなによりやってみたかったので実装しました。

コーディングする前は「まあ画面外に出たらロケットの座標を逆側の座標に変更するだけやろ...…」と思ったらなんか全然うまくいきませんでした。

いろいろ試行錯誤した結果うまくいかず、途方に暮れていたころにせっかくならとchatGPTを頼ってみることにしました。

chatGPTに聞いたことを参考にして書いてみたら普通にうまくいってウケました。ありがとうchatGPT。プログラミングの復習のついでにAIの使い方も少し学べるいい機会となりました。

~ゴール時の処理~

ゴールバーに触れたとき、ゴールタイムの保存とファンファーレを鳴らします。ファンファーレを流すためにコルーチン(処理に遅延を与えるもの)で少しだけ処理の遅延をしました。

ゴールタイム保存のために、コーディングを簡単にできるようplayerControllerでタイマーの処理をさせました。

ゴール時の処理の作成で音声を流す方法の復習だけでなく、処理の遅延やシーン間の値の保存・受け渡しの方法を学ぶことができました。ここだけで始めて知るプログラムがたくさんあったのでいい学びになりました。

カメラについて

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

public class CameraController : MonoBehaviour
{
    GameObject rocket;
    float currentheight = -3f; //カメラの高さの基準
    float destination = 245f; //ゴール地点でのカメラの高さ
    // Start is called before the first frame update
    void Start()
    {
        this.rocket = GameObject.Find("rocket");
    }

    // Update is called once per frame
    void Update()
    {
        float highest = this.rocket.transform.position.y; // カメラの高さ制限
        
        //自機が下に行っても高さのキープ
        if (highest > currentheight)
        {
            transform.position = new Vector3(transform.position.x, highest + 3f, transform.position.z);
            currentheight = highest;
        }

        //ゴール周辺でカメラの静止
        if (transform.position.y >= destination)
        {
            transform.position = new Vector3(0, destination, -10);
        }
    }
}

ロケットのy座標を受け取ってそれに合わせてカメラが追従します。

しかしそのまま追従させるとロケットが岩にぶつかってロケットの位置が下がるとカメラの位置も下がり、大事なスピード感が失われてしまうので、カメラの最高位置を保つプログラムを作りました。

このプログラムを導入することによる新たな問題として、ロケットが岩に押されに押されたときに画面外に出てしまいうる、といったものがありそうですがこちらの解決については後程。

頑張った処理のはなし

今回特に試行錯誤して頑張った点について2つありますのでお話させてください。

①画面外に出ることの抑制と岩の削除

先ほどの続きとなりますが、ロケットの最高位に合わせてカメラを固定すると、岩に押し込まれたときにロケットが画面外に出てしまいます。

また別の問題として、画面外に出た岩の処理の問題があります。
画面から岩が出ても処理としては消えているわけではないので、画面外の岩が溜まって次第に処理落ちしていく恐れがあります。

この2点の問題を解決するために下画面外にてborderスプライトの作成をしました。

黄緑で囲まれているやつ

実際は透明で当たり判定だけを与えています。横幅取って上にも伸ばしているのは少しでも岩がここに当たるようにするためですが、そこまで効果はなかったかも。

こいつがカメラに追従して動きます。このborderにタグ「border」を与えることで、岩が触れたときには消えます。またロケットが触れたときにはただぶつかるだけで物理的に画面外に出ることが抑制される、という仕組みです。

このやり方でロケットが画面外に出ないようにするには少し強引かなとは思いつつも、これ1つで岩を消して処理落ち対策もできるからいいかな~と思いこの仕様で実装することにしました。

②背景のスクロール

背景のスクロールについてですが、カメラの位置に合わせて2つの処理をします。

1.カメラがある位置になったら背景を上に生成
2.カメラがある程度離れたら下画面外の背景を削除

//1.カメラがある位置になったら背景を上に生成
//Update関数のみを抽出、背景prefabのプログラム

void Update()
    {
        float cameraPosy = this.MainCamera.transform.position.y; //カメラのy座標の取得

        //genrate_posよりカメラが高くなったらy座標10上に背景を生成
        if (cameraPosy >= generate_pos) 
        {
            GameObject go = Instantiate(backgroundPrefab);
            generate_pos += 10;
            go.transform.position = new Vector3(0, generate_pos, 0);
        }

    }
//2.カメラがある程度離れたら下画面外の背景を削除
//Update関数のみを抽出、背景へのプログラム


void Update()
    {
        float backgroundPos = transform.position.y; //生成された背景のy座標
        float cameraPos = this.MainCamera.transform.position.y; //カメラのy座標

        //カメラと距離が12離れたら破棄
        if (cameraPos - backgroundPos > 12)
        {
            Destroy(gameObject);
        }
    }

次々と上部に背景を生成し、画面外でもう映らない背景は消去する、これにより背景を使ってロケットのスピード感の表現、また余計な背景を消すことで処理落ち対策をする、というのができないか考えてコーディングしました。これ思いついて実装できたときめちゃ気持ちよかった。

自分で考えて打ったプログラムが思い通りに機能したときって本当にうれしい。自分がプログラミングで一番好きな瞬間です。

以上が自分で思案して考えたところです。ゲーム画面には映らない部分ですがこういうところもプレイの快適さ関わってくると思うのでこだわりました。

おわりに

読んでいただきありがとうございました。

世に出ている数多のゲームと比べると物足りない部分や拙いところは多々あります。それでも自分が挑戦して1から作ったゲームなので、自分がこだわったり悩み考えたところを知ってほしく筆を取りました。とりあえず1回ぐらいは遊んでいただけると幸いです。

他に作りたいゲームの案は試案の段階ですがいくつかあるので、それらも作って皆さんに公開できたらな、と思います。

現状では足りないことも多く、学ばなければならないことも数多くありますが、精一杯努力しますので応援いただけると幸いです。

プレイ先URL: https://kindness136.github.io/RocketRacer/

この記事が気に入ったらサポートをしてみませんか?