ゲーム制作勉強中!あこがれだったプログラマーに今からなろう!

昔、あこがれていたプログラマー。今からでも勉強してみようと思い立ち、チャレンジ開始! 勉強メモや、悪戦苦闘な日々の記録です。

Unityの関数の基礎の基礎

昨日に引き続き、しまづさんのyoutubeシリーズを参考に、3D脱出ゲームの勉強中です。

www.youtube.com

今日は、
# 04 アイテムボックスに左詰でいれる
# 05 アイテムのデータベース化
# 06 アイテムの選択
# 07アイテムの使用
を実習。

やっぱり新しいことを学ぶのは楽しい!😁
この時点でも、学びがたくさんありました。

    public bool OnSelected()
    {
        if (item == null)
        {
            
            return false;
        }

        BackGroundPanel.SetActive(true);
        return true;
        
    }

基礎の基礎で、関数の説明の時に、public void ってのが返り値を返さない関数って説明を見ていたはずなのに、
そこで、この例の様に、bool値を返す関数というものも学んだはずなのに、この概念がすっぽり頭から抜けていました🤣

そして、Bool値を返すだけでなく、他の処理も一緒にできるという、当たり前すぎることも全く頭にありませんでした💦
やっぱ本や動画で見ているだけでは身についてないなぁ・・・
実際にプログラムで活用していかないと覚えないなぁと、改めて思わされました。

それにしても、こんな程度の知識でもアクションゲームが作れたのだから、やっぱUnityってすごい🤣

あとは、ここまで学んで感じるのは、変数がとても少ないという事だろう。
僕が同じことをするなら、slotの格納状態を格納(言葉にするとなんてややこしい🤣)した変数を作りそう。
それに、slotの選択状態の表示切り替えは、先ほどの変数を使って、Updateでやりそうな気がする。

考えてみたら、Objectも変数の様に使えるわけだから、いちいち余計に変数を増やす必要がないんだよね。
この辺り、なんでも噛んでも変数を作ろうとするは、まだBASICの様な言語の考え方から切り替えできていないという事なんだろうな😅

ちょっと思い直して、3D脱出ゲームを作ることにしました。

前回、次はポケモンRPGと書きましたが、
3Dアクションゲームが3か月掛かったので、また長期間作成が必要な作品だと、ちょっとシンドイなあと思ってしまったので、
別のジャンルに挑戦することにしました。

という事で、しまづさんのyoutubeシリーズを参考に、3D脱出ゲームを作成してみます😄

www.youtube.com

まずはテクニックを学んでいきつつ、どんなゲームにするか考えていこうかなと思ってます。

今日は早速、
# 01 アイテムのクリック判定
# 02 アイテムの実装
# 03 アイテムボックスの実装
を実習。

アイテムでCubeやSphereだけでは寂しいので、フリーの3Dモデルサイト

free3d.com

から鍵🔑 のモデルをダウンロードして、それを使ってみました。

今回はどんなテクニックが学べるのか、今から楽しみです!

3Dアクションゲーム公開しました!

ついに3Dアクションゲームを公開しました!

unityroom.com

いや〜、かかったなあ😅
去年12月の2日から作成始めているので、実に3ヶ月もかかってしまいました。
プレイ時間は10分程度です🤣
時間にすると、たったこれだけのゲームですが、今回も学んだことをガッツリと詰め込みました。


今まで使っていなかったTimeLineの勉強ができたのが、とても良かったかな。
あと、だいぶUnityActionやScriptableObjectになれてきたと思います。

ゲームの中身としては、
右上のレーダー画面やロックオン機能、案内役の従者など、色々と作り込みましたが、
ジャンプなど、蛇足だったと思う機能もあったりします😆
そして、泣く泣く断念した機能も、当然あります。
スマホでも遊べるようにする機能が断念した機能で、バーチャルコントローラーも作っていたのですが
全体的に容量がデカくなりすぎて、どうやってもスマホで起動できませんでした😭

それでも、今回のゲームでスリム化についても色々と学べたので、次回作はもっと軽いものが作れるように頑張りたいです。

次はポケモンRPGを作るべく勉強中です!

α版のデバッグをしよう(バグの原因特定と解決②):3Dアクションゲームを作ろう(21)

前回に引き続き、3Dアクションゲームのα版のバグ取りを行っています。

前回見つけたバグ🪳は以下の通りです。
①回復用の魔法陣が一箇所でしか表示されない。
②一種類の敵の生成が止まってしまう。
③移動床に横から接触すると、移動床に引きずられてしまう。
④地面に沿うようにジャンプすると、ジャンプのままになってしまう。
⑤特定のタイミングで攻撃モーションのままになってしまう。そうなると移動不能状態になる。

この中で①と②は前回解決済みなので、今回は③から虫捕りを始めよう。

「③移動床に横から接触すると、移動床に引きずられてしまう。」
実はこのバグはUnity上のテストプレイでも確認しており、修正を忘れていたバグです。😅

移動床のスクリプトは、れーさんのサイトで説明されていたものを使用しています。

dkrevel.com

2Dゲーム用だけど、3Dでもバッチリ動く。
PlayerControllerのOnCollisionEnterで移動床と接触していると判定し、移動床との場合は移動床の移動速度を
取得してPlayerのRigidbody.Velocityに加算しています。
これにより、移動床に乗っているときは、移動床と同じようにPlayerが移動できますが、
ここまで書いていてわかる様に、移動床を踏まずに横に触れただけでも、移動床の移動速度を取得してしまうので
当然、移動床にPlayerが引きずられてしまう🤣

れーさんのスクリプトでは、Playerの高さと接触したポイントの座標から、移動床を踏んだのか接触したのかを判定
しているので、この問題は起きないようにしっかり対策されています。

そもそも、れーさんのゲームは2Dアクションで、敵を踏んだか敵にぶつかったかの判定も必要なので、この高さ判定の計算が
入っていますが、僕のゲームでは移動床しか判定の必要がありません。
それでもこの計算方法を使ってもいいのですが、どうせなら、もっとシンプルに修正することにしました。

移動床の動きとリンクするだけの一回り大きな床を作理、移動床を覆います。
次に移動床自体はMesh Rendererをoffにして透明に変更。
つまり、移動床に上からし接触できない様にする力技修正ですね🤣

・・・😅
とにかく、これでこの問題も解決。


「④地面に沿うようにジャンプすると、ジャンプのままになってしまう。」
ジャンプアニメーションの遷移は次の様になっています。


ジャンプキーが押されるとjumpフラグがtrueになり、上昇中のアニメに遷移、次にjumpフラグがfalseになれば下降中アニメに遷移します。
下降中アニメの状態(jumpフラグがfalse)で接地すれば着地アニメに遷移する仕組みですね。

肝心のjumpフラグがfalseになるのは、ジャンプキーが押されてから、空中にいる時間が0.3秒経過したタイミングにしていました。

        if(!IsGround)//接地判定がfalse、つまり空中状態。
        {

            flyingtime += Time.deltaTime;
            PlayerAnimator.SetFloat("flyingTime",flyingtime);
            if(flyingtime>0.3f)
            {
                PlayerAnimator.SetBool("jump",false);//空中にいる時間が0.3秒以上ならjumpフラグをfalseに変更。
            }
        }

つまり、ジャンプ時間が0.3秒以内で接地するような状況だと、jumpフラグがfalseでないため着地アニメに遷移できず、
ジャンプのままになるという理屈です。

なので、シンプルにジャンプキーが押されて0.3秒後に強制的にjumpをoffにするように修正しました。

            flyingtime += Time.deltaTime;
            PlayerAnimator.SetFloat("flyingTime",flyingtime);
            if(flyingtime>0.3f)
            {
                PlayerAnimator.SetBool("jump",false);//空中にいる時間が0.3秒以上ならjumpフラグをfalseに変更。
            }

考えすぎて、自分で複雑にしすぎていた様です。😅
これであっさり解決しました。😭

最後は、「⑤特定のタイミングで攻撃モーションのままになってしまう。そうなると移動不能状態になる。」
これが、なかなか出ないバグで、
テストプレイを繰り返しても全く発生してくれません。しかし何かのタイミングで不意にこのバグが発生します。
発生頻度が低すぎて調査ができずにいたのですが、スタジオしまづのメンバーにテストプレイをお願いしたところ、
Playerが攻撃モーション中にモンスターからダメージを受けた時に、このバグが発生する事をKatsumaさんが突き止めてくれました🙇

おかげで、こちらのバグも原因はアニメーション遷移にあるのではないかと仮定できました。

攻撃中に移動できると、攻撃モーションのまま横滑りしてしまうのですが、これが嫌で、
実は攻撃中には移動できないように制限をかける様にしており、これが原因ではないかと思う。
・・・思う。ってのは、これだけが、確証が取れてないからです😅
何せ、ほんとにこのバグ。狙ってもなかなか発生させられません。🤣

攻撃の仕組みはこんな感じです。

    void Attack()
    {
        if(Sta>9)//攻撃に必要なスタミナがあるか判定
        {
            if(inputSystemSC.Player.Fire.triggered)
            {
             IsMoveing=true;        

                if(PlayerAnimator.GetBool("Combo")==true && Sta>9) //コンボ判定2
                {
                    PlayerAnimator.SetBool("Combo2",true);
                }

                if(PlayerAnimator.GetBool("Attack")==true && Sta>9 ) //コンボ判定
                {
                    PlayerAnimator.SetBool("Combo",true);
                }
                else
                {
                    PlayerAnimator.SetBool("Attack",true); //通常攻撃
                    canMove=false; //ここで移動制限
                }
            }
        }
    }

移動制限の解除はアニメーション中のEventで行っています。

攻撃のアニメーションが終了すれば移動制限が解除されるので、問題がない様に見えますが、

被ダメージアニメーションへの遷移がAny Statusからになっているため、
攻撃アニメーションのタイミングによって、移動制限が解除されないパターンがあるのかもしれません。

とりあえず、被ダメージで移動制限や攻撃関係のフラグを全部リセットするように修正してみました。

    public void OnDamage()
    {
        PlayerAnimator.SetBool("Attack",false);
        PlayerAnimator.SetBool("Combo",false);
        PlayerAnimator.SetBool("Combo2",false);
        CanMove();
        if(NoDamage){return;}
        audioSource.PlayOneShot(damageSE);
        PlayerAnimator.SetTrigger("damage");
        HP=HP-20;
    }

このバグだけは、これで解消できたのか不安ですが、
これでパッと確認できたバグは対応できたと思います。

もう少しテストプレイを繰り返して、いよいよ一般公開しようと思います!

α版のデバッグをしよう(バグの原因特定と解決①):3Dアクションゲームを作ろう(20)

3Dアクションゲームのα版が完成しましたが、予想通り、多数のバグ🪳が発生😭

パッと見つかったのが以下の通り。
①回復用の魔法陣が一箇所でしか表示されない。
②一種類の敵の生成が止まってしまう。
③移動床に横から接触すると、移動床に引きずられてしまう。
④地面に沿うようにジャンプすると、ジャンプのままになってしまう。
⑤特定のタイミングで攻撃モーションのままになってしまう。そうなると移動不能状態になる。

嘆いていても仕方がないので、一つ一つ解決していこう。

「①回復用の魔法陣が一箇所でしか表示されない。」
魔法陣生成の仕組みは、特定のエリアにSphereCollider付きのObjectを設置してあり、そこにPlayerが侵入+PlayerのHPが9割以下であれば、パーティクルシステムで作成した魔法陣を再生するというもの。
それなのに、どのエリアのColliderに侵入しても、0番目のエリアで魔法陣が生成されてしまう😭
試しに、DebugでColliderが反応したエリア番号を表示させてみると、正しいエリア番号が表示される。
しかし、魔法陣が表示されるのはいつも0番目のエリア💦

色々と実験したけれど、解決の糸口が見つからない。

結局、いつものスタジオしまづさんのユニコンでジョニさんに相談し、ようやく解決策を見つけることができました。

修正前はOnTriggerEnterではエリア侵入のboolだけをたてて、魔法陣生成はUpdateで行っていた。
今にして思えば、これだと当然、他のエリアのObjectが反応することもあるわけで💦
なので、一番最初の0番がいつも魔法陣を生成するという、あたりまえな動きだった。
分かってしまえば解決方法は簡単で、魔法陣の生成をUpdateではなく、OnTriggerEnterで生成するように修正。
これだけで問題が解決しました🤣

てか、なんでUpdateで生成させていた、僕・・・_| ̄|○


「②一種類の敵の生成が止まってしまう。」
マップ上の敵は種類ごとに現在数がカウントされていて、設定した最大数に達するまでは、特定の間隔で生成され続ける仕組みになっている。
最大数に達しても、敵が倒されれば現在数が減り、生成が再開される。
なのに、


このフライングアイくんが最大数に達した段階で生成が止まってしまう。
生成されたフライングアイを全滅させてしまっても次が生成されない💦

しかも不思議なのは、Unity上のテストプレイでは正常に再生成されるのに、ビルドしたゲームだとこの状況が発生する💦

調べ方もわからないままに悩んでいたのだけど、結局、これもスタジオしまづさんのユニコンでの相談で、解決の糸口を見つけることができた。

敵の種類ごとの現在数をUIに表示するように修正して、再度ビルドして実験したみたところ、
フライングアイくんだけは倒しても現在数が減らないということがわかった。

現在数のカウントはGameManagerが管理しているが、加算は敵生成ObjectのEnemy Generatorスクリプトで行い、減算は各敵のControllerスクリプトで行っている。
そして、敵のControllerはスライムとオークは同じEnemy Controllerスクリプトだけど、フライングアイはBeholderControllerという別のスクリプトになっている。
そして、敵生成のEnemy Generatorはスライム・オーク・フライングアイと全てが同じスクリプトで生成させている。
その整合を取るために、生成した敵のComponentをtry文でystem.NullReferenceExceptionをトラップする方法で、生成した敵のControllerがEnemy ControllerとBeholderControllerのどちらか判定して設定する仕組みにしていた。

        try
          {
              gene_enemy.GetComponent<EnemyController>().enemyGenerator=this;
          } catch (System.NullReferenceException e)
          {
               gene_enemy.GetComponent<BeholderController>().enemyGenerator=this;
          }

このやり方でUnity上では問題なく動作してくれていたけれど、ここに問題があると仮定して、別の方法でControllerを判定するように修正してみた。

    [SerializeField] int GeneratorNo; //ここでGeneratorの番号を指定する。
    public GameObject Enemy1;
    [SerializeField]HostilityListManager hostilityListManager;
    [SerializeField] GameManager gameManager;

    void Start()
    {
        InvokeRepeating("EnemyGenerate",3f,15f);        
    }

    void EnemyGenerate()
    {

        if(gameManager.EnemyCount[GeneratorNo]<gameManager.MaxEnemy[GeneratorNo]) //指定した番号でGameManagerのカウントを識別。
         {
            GameObject gene_enemy=Instantiate(Enemy1,transform.position,Quaternion.identity);

            if(GeneratorNo==2) //生成する敵がこれで判定できる。
            {
         //倒された時に減算する変数を指定するために、Generatorの番号を敵のスクリプトに設定する。
                gene_enemy.GetComponent<BeholderController>().GeneratorNo=GeneratorNo; 
                gene_enemy.GetComponent<BeholderController>().itemManager=itemManager;
                gene_enemy.GetComponent<BeholderController>().enemyGenerator=this;
                gene_enemy.GetComponent<BeholderController>().gameManager=gameManager;
            }
            else
            {
         //倒された時に減算する変数を指定するために、Generatorの番号を敵のスクリプトに設定する。
                gene_enemy.GetComponent<EnemyController>().GeneratorNo=GeneratorNo;
                gene_enemy.GetComponent<EnemyController>().itemManager=itemManager;
                gene_enemy.GetComponent<EnemyController>().enemyGenerator=this;
                gene_enemy.GetComponent<EnemyController>().gameManager=gameManager;
            }

            gene_enemy.GetComponentInChildren<StatusManager>().hostilityListManager=hostilityListManager;
            gameManager.EnemyCount[GeneratorNo]++;
        }
    }

これでフライングアイくんを倒すと、代わりが正常に生成されるようになってくれました😆

②の様に、正常なものとバグになるものの二種がある場合は、まず「その二種の何が違いを生んでいるのか」を調べていくことが大切だという事を、早いうちに学べたのはとても良かったと思う。

それにしても、
Unity上のテストでは問題なく動作しても、ビルドすると思った通りに動作しなくなる処理があるんだね💦
これもとても勉強になったよ。
ほんと、早く解決できた良かった〜😅

GitHub CopilotとMicrosoft Copilot:ゲーム開発者の新たなパートナー

ここ数日間は、3Dアクションゲームの制作にずっと没頭してます。
2月中に完成させたいと思っていましたが、少し間に合わない感じ💦

基本的には、作るべきものの大半は作り終えているので、残りは全体的な調整とデバッグくらいですが、思った以上に時間がかかっています。
ちょっと気分転換に、新しいことを始めたくなり、これまで試していなかったGitHubとの連携、そしてGitHub Copilotの導入をやってみようと思いました。

GitHubとの連携は、スタジオしまづさんのYouTubeを見ながら、しまづさんの個人サロンでの説明を受けつつ、サクッと導入。
・・・したまでは良かったのですが、連携しようとしたゲームが、制作中の3Dアクションゲームということもあり、多くのアセットを使用しているため、容量が12Gを超えており、残念ながらGitHubとの連携はできませんでした。🤣

しかし、GitHub Copilotの導入は問題なくできたので、早速色々と活用しています😊

さすがにGPT-4とOPEN AIのCODEXを搭載しているため、既に作成したコードなどから、僕がやりたいことをガンガンと先読みしてくれます。😆

例えば、制作中のゲームでPlayerを別のObjectに切り替えようとしたときなど、別のObjectをPrefab化して変数に取り込み、Playerを消す処理を書いた時点で、

Instantiate(Prefab,Player.transform.position,Quaternion.identity);

を入力候補として表示してくれました。😳

そして、GitHub Copilotを契約すると、GitHub Copilot CHATも利用できるようになります。
これは文字通りチャットで、コード以外のUnityのコンポーネントの設定方法やメソッド、プロパティの意味や使い方などを質問できます。

そのおかげで、導入してからGoogleで検索する回数が、目に見えて減りました🤣

AIの恩恵を受けるようになり、そのつながりでAI関連の記事を読んでいるうちに、Microsoft Copilotというものもあることを知り、こちらもすぐに導入しました。

嬉しいことに、Microsoft Copilotは無料です!😆

両方のCopilotを使ってみた感想ですが、
コードを書くという一点に特化するなら、GitHub Copilotの方が断然良いです。

これは、GitHub CopilotがエディタであるVisual Studio Codeに搭載される形なので、これまでに書いたコードまで確認して次にやりたいことを推測、コード補完や提案する機能を持っているため、GitHub Copilotが優れているのは当然。

ただ、Unityでゲーム制作するとなると、コンポーネントの設定など、コード以外の要素を調べてる必要もできてますが、これらを調べるのであれば、両方のCopilotの差はなくなる感じでした。

コードに限らず、漠然とした質問でも大丈夫です。
例えば、「UIをキーボードで操作できないようにしたい」などを調べ場合、すでにこの知識を持っている人は、これがスクリプトで実現できると理解していますが、そうでない人には、これを実現するためにコンポーネントの設定を変えれば良いのか、スクリプトでできる事なのか、Unity自体の設定なのか、どうすれば実現できるのかわかりません。
Google検索などでも、なかなか、ズバリとした記事を探すのは大変です。

しかしながら、このような状況では、Microsoft CopilotもGitHub Copilot CHATもしっかりと回答を提示してくれます。

ちなみに、先ほどの例題は、Event Systemのインスペクターの設定やスクリプトで実現できます。

Event Systemのインスペクターの設定の場合は、
baba-s.hatenablog.com
こちらの記事も例題として紹介もしてくれます。

スクリプトの場合は下記で実現できます。

using UnityEngine;
using UnityEngine.EventSystems;

public class EventSystemController : MonoBehaviour
{
    private void Start()
    {
        // Event Systemを探す
        EventSystem eventSystem = FindObjectOfType<EventSystem>();
        if (eventSystem != null)
        {
            // Send Navigation Eventsをオフにする
            eventSystem.sendNavigationEvents = false;
        }
        else
        {
            Debug.LogWarning("Event Systemが見つかりませんでした。");
        }
    }
}


このような優秀な機能ですが、実に安価に導入できます。

GitHub CopilotはCHATも含めて月額10ドルです。
Microsoft Copilotは基本的な範囲であれば無料で利用できるので、併用してみるのも全然ありだと思います。

プレイヤーを誘導しよう!案内役の導入。:3Dアクションゲームを作ろう(19)

プレイヤーはこちらの思った通りに行動してくれるとは限らない。
どこかの動画で、そんなフレーズを聞いた覚えがあったけど、このゲームでもそれは当てはまると思う。

ジェムを出すまで敵を倒すってのは、こちらが想定している動きで、必ずしもプレイヤーがそう行動してくれるわけではない。
ジェムを出せたら出せたで、同じ敵ばかりを延々を倒すかもしれない。
かといって、それをシステム的に説明するのでは味気ないし🧐
それなら、自然にそれを説明してくれる存在がいれば良い。ってわけで案内役を作成することにした。

案内役はプレイヤーに追従して動いて欲しい。川があるから飛んでるキャラが良いなと、AssetStoreでアセットを見ていたら、妖精キャラが無料であったので早速採用。

assetstore.unity.com

・・・採用したんだけど、移動モーションが良い感じではなかったので、残念ながら変更。

assetstore.unity.com

idle状態のモーションが移動させていてもしっくりくるので本採用。
案内役、イーグル君です。

イーグル君の追従だけど、プレイヤーの子にしてしまうと、あまりにピッタリと追従してしまって不自然なので、自主的にプレイヤーを追いかける様にしたい。
追従する動きについてググったら、ぴったりの記事が😆

gomafrontier.com

イーグル君にアタッチするスクリプトTalkManagerの中の追従機能のメソッドとして、紹介されていたスクリプトをちょこっと改造。

基本的構造は変えていないけれど、一番最初でfollow Target、つまりプレイヤーとイーグル君の距離を測定して、
距離がある程度(0.2以下)近づいたら移動を止めて方向変換だけにした。

これで追従してくれる様になったが、敵の中には遠距離攻撃してくる奴もいるし、あまりにプレイヤーに近いところをうろうろしていると
攻撃モーションが被ってしまう。
イーグル君には攻撃が当たらないようにColliderを設定するにしても、モーションを避けさせるのは面倒なので、
戦闘中にはイーグル君には離れてもらった方が都合が良い。

じゃあ、戦闘中ってのをどうやって判定するか。
複数の敵が同時にプレイヤーと戦闘中になることも考えると、単純なBoolではうまくいかなさそう。

色々と考えていて、ふとロックオン機能で使ったListに思い至った。
敵がプレイヤーをTargetにしたら専用のListに敵自身を登録する様にして、
そのListのCountが1以上になったらイーグル君に退避行動を取らせればうまくいきそう。

自動的にListに登録する方法は、ロックオン機能で学んだので、その応用でいけるだろう。

www.youtube.com

List、敵意のListということでHostilityListと、それを管理するManagerを作成。
敵Objectの子にあるStatusManagerがプレイヤーをTargetする機能を持っているので、そこに

hostilityListManager.HostilityList.Add(Main.gameObject);

を追加して、Listに敵自身を追加させる。

敵がプレイヤーを見失ったらListから敵自身を抜くのも忘れないように

Main.Target=null;

for (int i = 0; i < hostilityListManager.HostilityList.Count; i++)
{   
    if(hostilityListManager.HostilityList[i]==Main.gameObject)
    {
        hostilityListManager.HostilityList.RemoveAt(i);
    }
}

そして、イーグル君のスクリプトTalk Managerで
hostilityListManager.HostilityList.Countが0でない時には退避行動を取らせる様にして、さらに追従行動も取らないようにしてやれば良い。
完成形の追従メソッドはこんな感じ。

    void Update()
    {
        if(hostilityListManager.HostilityList.Count!=0)
        {
            talkanimator.SetBool("Leave",true);//退避アニメに変更する。
        }

        DoAutoMovement();
    }

    protected void DoAutoMovement()
    {
        if(hostilityListManager.HostilityList.Count==0) //敵意Listが0でなければ追従開始
        {
            talkanimator.SetBool("Leave",false);//退避アニメを解除。
            float BirdDistance=Vector3.Distance(followTarget.position,transform.position);
            Quaternion move_rotation = Quaternion.LookRotation(followTarget.transform.position - transform.position, Vector3.up);
            transform.rotation = Quaternion.Lerp(transform.rotation, move_rotation, 0.1f);
            if(BirdDistance>0.2f) //イーグル君とプレイヤーの距離判定
            {   
                float movingspeed=move_speed*BirdDistance;
                movingspeed = System.Math.Min(movingspeed, move_speed);
                rb.velocity = transform.forward * move_speed*BirdDistance;
            }
            else //近すぎるなら移動は止めて方向変換だけにする。
            {
                transform.rotation=followTarget.rotation;
                rb.velocity=Vector3.zero;
            }
        }
    }

追従機能はできたので、肝心の案内。喋る機能の作成をしよう。

まずはイーグル君の発言を表示するウィンドウを作成する。


ヴァーチャルコントローラーがあるせいで、ちょっと左ずれだけどまぁいいだろう。

色々な記事や動画を見ていると、セリフは大抵の場合はScriptableObjectを作って、そこに書く様なので、
僕もそうすることにした。


Next Talk Noが0でなければ続きのセリフがある様にしている。

発言用のメソッドはこんな感じ。

    public void Talking(int talkno)
    {
        talkwindow.SetActive(true); //発言ウィンドウを表示
        
        foreach(TalkData i in TalkSO.TalkEntity.talkDataList) //ScriptableObjectからセリフを拾ってくる。
        {
            if (i.talkNo != talkno) continue;
            talkText.text=i.talkText;
            _nextTalkNo=i.nextTalkno; //次のセリフが設定されていれば、それも拾ってくる。
        }

        StartCoroutine(FadeIn(2.5f,_nextTalkNo));
    }

FadeInの説明だけど、どうせなら発言は一気に出るのではなく、一文字ずつ表示する様にさせたいので、

logicalbeat.jp

この記事からFadeInやSetTextAlphaを使わせてもらった。
ついでに、ここにnextTalknoの判定と次のセリフ表示の機能を、ちょこっと追加。

    private IEnumerator FadeIn(float waitsec,int nextno)
    {

        // script上でテキストを更新した場合、TMPの更新が終わっていない場合があるので再生成
        talkText.ForceMeshUpdate(true);
        TMP_TextInfo textInfo = talkText.textInfo;
        TMP_CharacterInfo[] charInfos = textInfo.characterInfo;

        // 全ての文字を一度非表示にする(特殊文字の兼ね合いで要素と文字の数が一致しない場合がある)
        for (var i = 0; i < charInfos.Length; i++)
        {
            SetTextAlpha(talkText, i, 0);
        }

        // charInfosの要素数分ループ
        for (var i = 0; i < charInfos.Length; i++)
        {
            // 空白または改行文字の場合は無視
            if (char.IsWhiteSpace(charInfos[i].character)) continue;

            // 一文字ごとに0.05秒待機
            yield return new WaitForSeconds(0.05f);
            player.TalkSE();//一文字表示するたびに音を出す。

            SetTextAlpha(talkText, i, 255);
        }

        yield return new WaitForSeconds(waitsec);
        talkText.text="";
        talkwindow.SetActive(false);

        if(nextno!=0)//引数で受け取ったnext Talk Noが0ではなければ続きのセリフがある。
        {
            //talkwindowが閉じられるまで待つ。
            yield return new WaitUntil(()=>(talkwindow.activeInHierarchy!=true));
            yield return new WaitForSeconds(0.5f);            
            Talking(nextno);
        }

    }

これで追従と発言機能が完成したので、案内させたい場所で、各種のスクリプト達からこのTalkingメソッドを呼んでやれば良いだろう。

完成形はこんなイメージ。

youtu.be