見出し画像

【Unity】Unity1Week【1ボタン】に参加!【備忘録】


Unity1週間ゲームジャムとは

ゲーム投稿サイトunityroomで不定期に開催されているUnityを使用して1週間でゲームを作成・投稿し、評価しあうという、ゲーム開発者必見の頭のおかしいイベントです。自分は6回目の参加です。今回は期間が2023年9月18日(月) 0時〜2023年9月23日(日) 20時でお題は【1ボタン】でした。

結構、開発するゲームの幅が限られるお題だとは思いましたが、以前から温めていたゲームのアイデアがぴったりだったため、アイデア出しに苦労はしませんでした。

投稿したゲーム「ヒグマだけど、鮭ぶっ飛ばしてみた」

タイミングよくひっかきボタンを押して、遡上してきた鮭をぶっ飛ばすゲームです。

タイミングがかなりシビアですが、1プレイがかなり短いので(30秒くらい)、是非遊んでみてください。以下、備忘録です。

1.ゲームシステムの設計

元は以下の参考記事のような野球のバッティングのようなシステムを考えていました。ミートカーソル的な奴があったり、飛距離がどれくらい出るか競ったり…でも、なんかピンと来ないので辞めました(要約:理解できなかった)。

次にスライダーを利用したタイミングゲームを思いつきましたが、それでは前のゲームジャムで出したゲームと丸被りなので辞めました。

そこで考えたのが以下のシステムです。

  1. 鮭が遡上するタイムラインを作成する。

  2. ひっかきボタンをプレイすると、熊が鮭の遡上するルート上(ひっかきゾーン)をひっかく。

  3. 熊がひっかいた一瞬、ひっかきゾーンのボックスコライダーが有効になる。

  4. 3のとき、鮭のボックスコライダーがひっかきゾーンのどこに位置していたかで、得点が変化する。

結局はタイミングゲームなのですが、鮭の動きと音でタイミングをつかむしかない、なかなか厳しい難易度のゲームです。

2.Terrainで地形を作成

まずTerrainで地形を作成しました。久々だったので不安でしたが、以下の記事を見ながら行えば簡単でした。

川については、SetHeightで溝を掘り、SimpleWaterShaderURPのPrefab、WaterBlock_50mで埋めました。

木はConifersを使用しましたが、URPでピンク色になってしまうオブジェクトだったので、今回はMKToonShaderを使用しました。有料アセットですが、バンドルに入っていたようで、知らぬ間に積みアセットになっていました。簡単にピンク色が解消できたのでいい発見でした。
積みアセットが減ったよ!やったねたえちゃん!

3.ヒグマの実装

ヒグマはForest animalsに入っていました。これもバンドルに入ってました。定価で買うと220ドルもする高級アセットです。

高級アセットだけあり、アニメーションの種類も大量です。クマもオス、メス、子熊とあります(正直オスとメスは何が違うかさっぱりわかりません…。)。今回はオスを使いました。
アニメーションが多いままだと混乱するので、必要なアニメーションだけをアニメーター上に残します。
そしてBaerControllerを作成し、熊にアタッチします。

using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class BearController : MonoBehaviour
{
    public Animator characterAnimator; // キャラクターのアニメーターコンポーネントを格納する変数
    public Button attackBtn;//ひっかきボタン
    private static readonly int Attack1 = Animator.StringToHash("Attack");
    public BoxCollider [] hitZoneCollier;//ヒットゾーンのボックスコライダーを格納する配列
    public AudioClip swingSe;//スイング音
    public float _preparationTime;//ボタンを押してからヒットゾーンが有効になるまでの時間
    public float _waitTime;//ヒットゾーンが無効になるまでの時間

    public void Start(){
        //ヒットゾーンのボックスコライダーコンポーネントを全て無効にする
        for (int i = 0; i < hitZoneCollier.Length; i++){
            hitZoneCollier[i].enabled = false;
        }
    }

    public async void Attack(){
        // Attackトリガーを有効にする
        characterAnimator.SetTrigger(Attack1);
        //_preparationTime待つ
        await UniTask.Delay(TimeSpan.FromSeconds(_preparationTime));
        //ヒットゾーンのボックスコライダーコンポーネントを全て有効にする
        for (int i = 0; i < hitZoneCollier.Length; i++){
            hitZoneCollier[i].enabled = true;
        }
        //スイング音を鳴らす
        SoundManager.instance.PlaySe(swingSe);
        //_waitTime待つ
        await UniTask.Delay(TimeSpan.FromSeconds(_waitTime));
        //ヒットゾーンのボックスコライダーコンポーネントを全て無効にする
        for (int i = 0; i < hitZoneCollier.Length; i++){
            hitZoneCollier[i].enabled = false;
        }
    }
}

熊がひっかくアニメーションをすると、_preparationTime待ち、ひっかきゾーンのボックスコライダーを有効にします。そして_waitTime待って、ひっかきゾーンのボックスコライダーを無効にします。
ひっかきゾーンは5つに分かれており、それぞれにタグがついています。

ゲーム中だとHomeRunがJustMeetになってます。これもうわかんねぇな
うわぁ~やっぱり怖ぇよぉ

4.鮭の実装(TimeLine)

鮭は始めはスクリプトで動かそうと思ってましたが、TimeLineのほうが圧倒的に楽なことに気が付きました。動画投稿者にとってTimeLineは本当にとっつきやすいです。
最近全く動画投稿できてませんが。
以下の記事が参考になります。

そして以下が鮭にアタッチするスクリプトです。

using System;
using UnityEngine;
using UnityEngine.Playables;

public class FishMover : MonoBehaviour{
    public BoxCollider boxCollider;//鮭のボックスコライダー
    public AudioClip meetSe;//インパクトSE
    public AudioClip justMeetSe;//良インパクトSE
    public AudioClip homeRunSe;//最良インパクトSE
    public PlayableDirector salmonDirector;//鮭のタイムライン
    public PlayableDirector meetDirector;//インパクトしたタイムライン
    public PlayableDirector justMeetDirector;//良インパクト↓タイムライン
    public PlayableDirector homeRunDirector;//最良インパクトタイムライン
    public GameObject tooSlowText;//「タイミング:おそい」というテキスト
    public GameObject tooEarlyText;//「タイミング:はやい」というテキスト
    public GameObject slowText;//「タイミング:少しおそい」というテキスト
    public GameObject earlyText;//「タイミング:少しはやい」というテキスト
    

    private void Start(){
        //全てのタイミングに関するテキストを非アクティブにする。
        tooSlowText.SetActive(false);
        tooEarlyText.SetActive(false);
        slowText.SetActive(false);
        earlyText.SetActive(false);
    }

  //鮭がSetActive(true)になったとき
    private void OnEnable(){
        //鮭のボックスコライダーを有効にする。
        boxCollider.enabled = true;
        //鮭のタイムラインを再生する
        salmonDirector.Play();
    }

    //ヒットゾーンに当たったら呼び出される。
    private void OnTriggerEnter(Collider other){
        salmonDirector.Pause();//鮭のタイムラインを停止する
        salmonDirector.time = 0.0f;//鮭のタイムラインを初めまで戻す
        boxCollider.enabled = false;//鮭のボックスコライダーを無効にする。有効のままだと連続で呼び出される。
        GameManager.instance.comboCount++;//コンボカウントを1プラスする。
        if (other.CompareTag("HomeRun")){//当たったヒットゾーンのタグがHomeRunなら
            SoundManager.instance.PlaySe(homeRunSe);//Se鳴らす
            //スコアに加算する
            GameManager.instance.score += 100 * GameManager.instance.comboCount;
            homeRunDirector.Play();//最良インパクトのタイムラインを再生する。
        } else if (other.CompareTag("JustMeet(early)")){//当たったヒットゾーンのタグがJustMeet(early)なら
            justMeetDirector.Play();//良インパクトのタイムラインを再生する
            GameManager.instance.score += 30 * GameManager.instance.comboCount;
            SoundManager.instance.PlaySe(justMeetSe);
            earlyText.SetActive(true);//少しはやいのテキストをアクティブにする。
            slowText.SetActive(false);//少しおそいのテキストを非アクティブにする
        } else if (other.CompareTag("JustMeet(Slow)")){
            justMeetDirector.Play();
            GameManager.instance.score += 30 * GameManager.instance.comboCount;
            SoundManager.instance.PlaySe(justMeetSe);
            earlyText.SetActive(false);//プレイするタイムラインが「少し遅い」と同じなので、
            slowText.SetActive(true);//タイミングに関するテキストは、アクティブと非アクティブを切り替えて対応
        } else if(other.CompareTag("early")){
            GameManager.instance.score += 10 * GameManager.instance.comboCount;
            SoundManager.instance.PlaySe(meetSe);
            meetDirector.Play();
            tooEarlyText.SetActive(true);
            tooSlowText.SetActive(false);
        } else if(other.CompareTag("Slow")){
            GameManager.instance.score += 10 * GameManager.instance.comboCount;
            SoundManager.instance.PlaySe(meetSe);
            meetDirector.Play();
            tooEarlyText.SetActive(false);
            tooSlowText.SetActive(true);
        }
        GameManager.instance.ScoreComboDisplay();//スコア表示を修正する。
        GameManager.instance.flySalmon++;//ぶっ飛ばした鮭の数を加算
    }
}

ポイントは鮭のボックスコライダーと、ひっかきゾーンのどのボックスコライダーが接触したかで、次に再生する鮭が吹っ飛ぶタイムラインを分けていることです。

ゲーム中だとHomeRunTimeLineがJustMeetになってます。これもう

なおJustMeet(early)とJustMeet(Slow)、earlyとslowで同じタイムラインを使用するためにタイムライン再生中に画面上部から降りてくるテキストの文面をスクリプトで切り変えています。

slowのタイミングでとらえたとき表示されるテキスト
earlyでとらえたときは、SlowTextが無効になり、earlyTextが有効になる
SalmonTimeLine

またOnTriggerEnterで

salmonDirector.Pause();//鮭のタイムラインを停止する
salmonDirector.time = 0.0f;//鮭のタイムラインを初めまで戻す

としているのもポイントです。salmonDirectorを停止し、初めまで巻き戻し(死語)しないと、次の鮭をSetActive(true)にしたとき、途中から始まってしまいます。借りたビデオは最初まで巻き戻そうね!

HomeRunMeetTimeLine

あと鮭のボックスコライダーはOnTriggerEnterでboxCollider.enabled = false;にしないと連続で鮭がぶっ飛んで大変なことになります。

5.GameManagerの実装(TimeLine)

GameManagerは以下の通りです。
コード中のコメントを読んでもらえれば大体わかると思います。

using System;
using UnityEngine;
using UnityEngine.UI;    

  [SerializeField] private AudioClip startSe;//スタートSE
    [NonReorderable] public int highScore = 0;//ハイスコア
    [SerializeField] private Text scoreText;//スコアテキスト
    [Header("ハイスコアテキスト")] public Text highScoreText;
    [Header("ハイスコア更新テキスト")] public GameObject highScoreBroken;
    [Header("コンボテキスト")] public SuperTextMesh comboText;
    [Header("コンボテキスト")] public GameObject _comboText;
    [Header("サーモンカウント")] public Text salmonCount;
    [SerializeField] GameObject salmon;  //鮭
    private int playCount;
    [NonSerialized] public int score;//得点
    [NonSerialized] public int comboCount;  // 現在のコンボ数
    //[NonSerialized] public bool manual;
    [SerializeField] private GameObject _startManual;
    [Header("結果のスコアテキスト")] public Text resultScoreText;
    [Header("結果の鮭テキスト")] public Text resultSalmonText;
    [NonSerialized] public int flySalmon; //ぶっとばした鮭の数
    [SerializeField] public GameObject resultPanel;
    
    public static GameManager instance;//どこからでもアクセスできるようにする
    void Awake() {
        CheckInstance();
    }
    void CheckInstance() {
        if (instance == null) {
            instance = this;
        } else {
            Destroy(gameObject);
        }
    }

    //マニュアル経由で開始した場合。マニュアルの開始ボタンにセット
    public void OnClickStart(){
        _startManual.SetActive(false);
        Time.timeScale = 1;//時を進める
        SoundManager.instance.PlaySe(startSe);
        ES3.Save<int>("Manual",1);//マニュアル表示済みフラグオン
    }
    
    private void Start(){
        // ゲームの初期化
        score = 0;//スコアを0にセット
        Time.timeScale = 0;//StartTimeLineが開始しないように時間を止める
        highScore = ES3.Load<int>("HIGHSCORE",0);//ハイスコアをロード
        highScoreText.text = highScore.ToString("d5");//ハイスコアテキスト更新
        highScoreBroken.SetActive(false);
        comboCount = 0;  // コンボ数をリセット
        flySalmon = 0;
        _comboText.SetActive(false);
        int flag = ES3.Load<int>("Manual", defaultValue: 0);//マニュアル表示フラグを呼び出す
        _startManual.SetActive(true);//マニュアルを表示
        if (flag == 1){//マニュアル表示済みなら
            _startManual.SetActive(false);//マニュアルを非表示に
            Time.timeScale = 1;//時を進める
        }
    }
    
    //TimeLineのレシーバーから呼ばれる
    public void ComboReset(){
        _comboText.SetActive(false);
        comboCount = 0;
    }

    //TimeLineのレシーバーから呼ばれる
    public void SalmonEnable(){
        salmonCount.text = "鮭残り"+ (10 - playCount) + "匹";
        if (playCount >= 10){//鮭が10匹遡上していたら
            GameEnd();   
        }else{
            salmon.SetActive(true);
            playCount++;
        }
    }

    public void ScoreComboDisplay(){
        scoreText.text = score.ToString("d5");
        if (comboCount >= 2){
            comboText.text = comboCount + "連続";
            _comboText.SetActive(true);
        }
    }
    
    private void GameEnd(){
        // Type == Number の場合
        naichilab.RankingLoader.Instance.SendScoreAndShowRanking(score);
        //現在のスコアがハイスコアより高いなら
        if (score > highScore){
            //現在のスコアをハイスコアとして記録
            ES3.Save<int>("HIGHSCORE",score);
            highScoreText.text = score.ToString("d5");//ハイスコアテキスト更新
            highScoreBroken.SetActive(true);//ハイスコア更新テキスト表示
        }

        resultScoreText.text = score.ToString() + "点";
        resultSalmonText.text = flySalmon.ToString() + "匹";
        resultPanel.SetActive(true);
    }
}

マニュアルの表示についてですが、まずTitleシーンに以下のようなコードを持ったStartManagerを置いています。
Tilteシーンを通ると、Manualフラグが0になり、プレイ前に必ずマニュアルが表示されるようになります。
しかしMainシーンからリトライでMainシーンに遷移した場合は、Manualフラグは1のままで、マニュアルは表示されません。

using UnityEngine;

public class StartManager : MonoBehaviour{
    public void Start(){
        //タイトル画面を通った後にゲーム開始すると、マニュアルを表示する。
        //Manualが0だとマニュアル表示、1ならマニュアルは表示しない。
        //タイトル画面を表示するとManual=0になる。
        ES3.Save<int>("Manual",0);
    }
}

6.カウントダウンアニメーションの実装

これまで作成したゲームでカウントダウンを使用する際はGameManagerなどからスクリプトでTextを動かしたりしていたが、これも圧倒的にTimeLineのほうが楽でした。

StartTimeLine

数字が素早く画面上部から降りてきて、中央付近でゆっくりになって、また素早く降りて画面下部に消えていくという動きが簡単に実装できました。
文字の動きに音を合わせるのも簡単ですし、もうTimeLineなしでは生きられません。

8.反省点

今度のゲームジャムは準備万全で行こうと9月頭から思っていましたが、西門郷介のiOSビルドで死にかけたため、あまり事前準備ができませんでした。
あとゲームジャム期間中に、カイロソフトの「創作ハンバーガー堂」を2日プレイしてしまいました。
つまり今回のゲーム出来がイマイチだとしたら、アップルとカイロソフトのせいです。

9.今後のアップデート予定

初めからスマホ用にリリースする予定でしたので、不具合や改善点があれば修正は続けていきます。
スマホ版からになる可能性が大ですが、ポイントで熊の情報を集められるコレクション要素も追加する予定です。
一応、今回のゲームを作った動機としてはクマに対する注意喚起もありますので…。

あとTerrainで作った森もビルドしたら木がスッカスカになってるんですが、それは…。ビルドのラインとガンマとかもよくわかってないんですが、そこらへんが関係あるんでしょうかね?

<その他>参考にした記事

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