メインコンテンツへスキップ
見出し画像

【プロジェクト:Neith】みんなの考えた最強スキル作成〜みんなで作るゲーム制作【5/3 16日目】

    ■今回やった事

    ・ネイトのスキルピックシステム作成

    ・スキルシステム完成

    ・アルカステラの待機モーション完成


    ■募集中のアイディア
    ①「樹海編」で発生しそうなイベント
    (例:森の中、クマさんに出会った。お化けと遭遇 など)
    ※期限はありません!皆さんの自由でユニークな発想をお待ちしています。

    ② 主人公・ネイトの「スキル」(無期限)
    (例:ファイアボール / 射程:2マス先の一体 / 効果:対象に小ダメージ)
    ※王道からちょっと笑えるネタスキルまで、
    どんなアイディアでも大歓迎です!


    こんにちは、まる。です!

    気が付けば、この企画の期限も残りあと2週間となりました。


    ペースとしてはギリギリ……
    いや、正直かなり厳しい状況ではありますが、今はとにかく前へ進むのみ!


    そして今回は、現在募集しているアイディアの中から、
    ついにネイトがスキルを獲得するようです😳


    皆さんと一緒に育ててきたネイトちゃんの勇姿を、
    どうか最後まで見届けてください!


    それでは、進撃の開発16日目……開幕です!


    二人の茶番

    🎧今日の作業用BGM

    10月の彗星、キミと出会う。/作(編)曲 :フリーBGMチャンネル Unique Gear
    【フリーBGM】10月の彗星、キミと出会う。

    画像
    ポマイラ(自称ネイトの使い魔)

    「よお、お前ら!オレだよ、ポマイラ様だ!
    最近、めちゃくちゃ暑くねぇか?
    今年も夏がやって来るのが早ぇ気がするぜ!
    お前らも熱中症には気を付けろよな!」

    画像
    ネイト(幻の星の精霊)

    「…すごいなぁ」

    画像
    ポマイラ(自称ネイトの使い魔)

    「あん? ……おいネイト!
    お前、オレ様が今、有難い話をしてるのに無視してX(旧Twitter)見てるとは良い度胸じゃねぇか!」

    画像
    ネイト(幻の星の精霊)

    「ふふ、ごめんなさい。
    つい、タイムラインに流れてきたゲーム開発者さんの動画が面白くて」

    画像
    ポマイラ(自称ネイトの使い魔)

    「全く、お前は24時間ずっとゲーム開発のこと考えてるよな…
    それで、どんな動画なんだよ」

    画像
    ネイト(幻の星の精霊)

    「いっぱいあるわよ。
    私と同じようにSRPGを自作している方から、アクション、RPG……とにかく沢山」

    画像
    ポマイラ(自称ネイトの使い魔)

    「最近、ゲーム開発者増えた気がするよな…
    やっぱ、生成AIで気軽に作れるようになったからか?」

    画像
    ネイト(幻の星の精霊)

    「確かに、それは一番大きいわね。
    インディーゲーム開発が活気づくのは良いことだわ」

    画像
    ポマイラ(自称ネイトの使い魔)

    「おいおい、ネイトさんよ。
    ちょっと暢気のんきすぎだぜ?
    気軽に作れるってことは、それだけライバルが増えるってことじゃねぇか。
    現に、今のお前より凄そうなゲーム作ってるやつなんてごまんといるぞ」

    画像
    ネイト(幻の星の精霊)

    「ふふ、そうね。
    でもね、ポマイラ。
    私、どうしてかそれが嬉しいのよ」

    画像
    ネイト(幻の星の精霊)

    「みんながこれだけ凄いゲームを作れるなら、
    私にもまだ可能性があるってことでしょ?

    それにね、どうしてか本当に好きな分野では、
    自分より凄い作品を見ても『悔しい』って気持ちより『凄いなぁ』って感想しか不思議と出ないのよ」

    画像
    ネイト(幻の星の精霊)

    「私、本当は結構負けず嫌いなのに……なんでだろうね?」

    画像
    ポマイラ(自称ネイトの使い魔)

    「……まぁ、それだけゲーム開発がお前にとって楽しいものなんだろ。
    勝ち負けじゃなく、単純に好きなものだから。
    いい作品を愛しちまうんだろうな」

    画像
    ネイト(幻の星の精霊)

    「あら、今日は珍しく良いこと言うのね。
    よし!それじゃ、そんな大好きなゲーム開発を始めていきましょうか!」



    スキルピックシステムを作成(作業時間:約3時間)

    画像
    ネイト(幻の星の精霊)

    「今日は、これまでみんなから頂いた私のスキル案を実装していくわよ!」

    画像
    ポマイラ(自称ネイトの使い魔)

    「ついにか…!
    それで、どの案を採用するんだ?」

    画像
    ネイト(幻の星の精霊)

    「何言ってるの、ポマイラ。
    そんなの『全部』採用するに決まってるじゃない」


    …



    画像
    ポマイラ(自称ネイトの使い魔)

    「はぁあああああああああああああああ!?!?
    全部ゥ!? お前、正気か!?
    大体、それ前にジェンスパークさんに止められただろうが!!!!」

    画像
    ネイト(幻の星の精霊)

    「そうね。」

    画像
    ポマイラ(自称ネイトの使い魔)

    「そもそも、ゲームバランスおかしくなるだろ!
    それに、スキル組み上げようとも実装の手間やバグのリスクもあるし…
    無茶だぜ、そんなの。
    新しい機能を追加するんじゃなくて今は仕上げる時期だぜ?ネイト!」

    画像
    ネイト(幻の星の精霊)

    「あら、ジェンスパークさんの言葉そのまんまじゃない」

    画像
    ポマイラ(自称ネイトの使い魔)

    「…とにかく!
    オレ様は反対だからな!
    大人しく三つくらいにしておけって!」

    画像
    ネイト(幻の星の精霊)

    「私も、この決断はかなり無茶をしていると思ってるわ。
    …でもね」

    画像
    ネイト(幻の星の精霊)

    「私の権能『世界を編み上げる力』は、画面の向こうにいるみんなと私を繋ぐ、大切な力なの。
    私はこの自分の能力に誇りを持っているわ。
    だから、みんなのアイディアは一つも無駄にしたくないの」


    ※権能とは?
    アルカステラが個別に持つ特別な力。
    ネイトの権能『世界を編み上げる力』は、異なる世界の声を力に変えて、
    世界のルールを書き換えるという強力な力なのだ。

    画像
    ポマイラ(自称ネイトの使い魔)

    「…お前にとって、その権能はプロジェクトが破綻するリスクを冒してでも守りたいものなのかよ」

    画像
    ネイト(幻の星の精霊)

    「ええ。」

    画像
    ポマイラ(自称ネイトの使い魔)

    「だったら、オレ様はもう止めねぇよ。
    思う存分、その世界ってやつをみんなの力で編み上げてやれ!ネイト!」

    画像
    ネイト(幻の星の精霊)

    「…ふふ、ありがとう。ポマイラ。」



    スキルピックシステムを作成

    画像
    ポマイラ(自称ネイトの使い魔)

    「それで、ジェンスパさんに逆らうからには算段はあるんだろうな?」

    画像
    ネイト(幻の星の精霊)

    「そうね。
    まずは、これを見てもらおうかしら」

    ■ネイトのスキルピックシステム作成 動画


    画像
    ポマイラ(自称ネイトの使い魔)

    「お、おいおい…マジかよ。
    もう組み上げちまってる」

    画像
    ネイト(幻の星の精霊)

    「まぁ、ほとんどがClaudeさんのお陰だけどね!
    今回は前回の反省を活かして、しっかりと指示を出しながら一つずつ確認して実装していったわ」

    画像
    ポマイラ(自称ネイトの使い魔)

    「…ところで、これはどんな仕様のシステムなんだ?」

    画像
    ネイト(幻の星の精霊)

    「ヴァンサバ(Vampire Survivors)のような、ローグライク系のベーシックなスキル獲得システムね。

    獲得していないスキル一覧の中からランダムに3つ抽選して、そこからスキルを一つ獲得するシステムよ。
    スキルには『重み(Weight)』という値があって、強いスキルほど出にくい仕様になっているの」

    ・実際の抽選処理のコード(NateSkillPool.cs)

    // NateSkillPool.cs
    // 抽選対象となる全スキルを管理する ScriptableObject。
    //
    // 【使い方】
    //   1. Unity メニュー → Assets → Create → TrainingGame → NateSkillPool でアセット作成
    //   2. Inspector の skills リストに NateSkillData アセットを登録する
    //   3. NateSkillManager の skillPool フィールドにこのアセットをアサインする
    //
    // 【抽選の重み付け】
    //   各 NateSkillData の weight フィールドが大きいほど出やすくなる。
    //   例: 通常スキル weight=10、レアスキル weight=3
    
    using System.Collections.Generic;
    using UnityEngine;
    
    namespace TrainingGame
    {
        [CreateAssetMenu(menuName = "TrainingGame/NateSkillPool", fileName = "NateSkillPool")]
        public class NateSkillPool : ScriptableObject
        {
            [Tooltip("抽選対象のスキル一覧。weight が大きいほど抽選されやすい。")]
            public List<NateSkillData> skills = new List<NateSkillData>();
    
            /// <summary>
            /// 重み付き抽選で重複なく count 件のスキルを返す。
            /// 既所持スキルを除外してから抽選する。
            /// プール内の候補が count 未満の場合は候補を全件返す。
            /// </summary>
            /// <param name="ownedSkillIds">既に所持しているスキルIDのセット(除外対象)</param>
            /// <param name="count">抽選件数(通常は3)</param>
            /// <returns>抽選されたスキルのリスト</returns>
            public List<NateSkillData> DrawRandom(HashSet<string> ownedSkillIds, int count = 3)
            {
                // 候補を絞る(未所持スキルのみ)
                var candidates = new List<NateSkillData>();
                foreach (var skill in skills)
                {
                    if (skill == null) continue;
                    if (string.IsNullOrEmpty(skill.skillId)) continue;
                    if (ownedSkillIds != null && ownedSkillIds.Contains(skill.skillId)) continue;
                    candidates.Add(skill);
                }
    
                var result = new List<NateSkillData>();
                int draw = Mathf.Min(count, candidates.Count);
    
                for (int i = 0; i < draw; i++)
                {
                    // 総重みを計算
                    int totalWeight = 0;
                    foreach (var c in candidates)
                        totalWeight += c.weight;
    
                    if (totalWeight <= 0) break;
    
                    // ランダムに1件選ぶ
                    int roll = Random.Range(0, totalWeight);
                    int cumulative = 0;
                    NateSkillData chosen = null;
                    foreach (var c in candidates)
                    {
                        cumulative += c.weight;
                        if (roll < cumulative)
                        {
                            chosen = c;
                            break;
                        }
                    }
    
                    if (chosen == null) break;
    
                    result.Add(chosen);
                    candidates.Remove(chosen); // 同じスキルが重複して出ないよう除外
                }
    
                return result;
            }
    
            /// <summary>skillId から NateSkillData を検索して返す。見つからなければ null。</summary>
            public NateSkillData FindById(string skillId)
            {
                if (string.IsNullOrEmpty(skillId)) return null;
                foreach (var skill in skills)
                {
                    if (skill != null && skill.skillId == skillId)
                        return skill;
                }
                return null;
            }
        }
    }
    
    画像
    画像
    ネイト(幻の星の精霊)

    「獲得していないスキルの中からランダムに3つ抽選するの。
    そのスキルは、重みっていう値があって強いスキルほど出にくい仕様になってるのよ。」

    画像
    画像
    ネイト(幻の星の精霊)

    「リストに載るアイテムは、いつもの『ScriptableObject(スクリプタブルオブジェクト)』で管理しているわ。

    これをスキル単位で細かく設定して、さっきのリストに追加していくのよ。
    ……スキルの作り方は話すと長くなっちゃうから、また今度にさせてね」

    画像
    画像
    ネイト(幻の星の精霊)

    「あと、それぞれのスキルにはアイコンを用意したわ!
    背景の色やフレームの豪華さで、レアリティや効果内容を何となく分類させてるの。
    とは言え、全部生成AIに作成してもらったから雰囲気の統一感は……あまりないのだけど」

    画像


    画像
    ネイト(幻の星の精霊)

    「『宴』からスキルピック画面を呼び出して、この画面からスキルをチョイスするのよ。
    そうしたら、私がスキルを獲得してまた宴のアドベンチャーパートに戻るって仕組み!」


    画像
    画像
    ネイト(幻の星の精霊)

    「あと、私のステータス画面にも獲得したスキルを表示するように改修したわ!
    ちなみに、頂いたアイディアのスキルは全部実装したから、
    追加のアイディアをまだまだ待ってるわよ!」

    画像
    ポマイラ(自称ネイトの使い魔)

    「……本当に全部作りやがったよ。
    実装、苦労しただろうに。ネイト、お前の執念は筋金入りだな」

    画像
    ネイト(幻の星の精霊)

    「ふふん、私の諦めの悪さを甘く見てもらったら困るわ!」


    スキルシステムの作成&待機モーション(8時間くらい)

    画像
    ネイト(幻の星の精霊)

    「…よし、仲間のスキルも完成。
    あとは、戦闘シーンにこのスキルのコマンドを反映させていくだけね」


    ■スキルシステム完成動画


    画像
    ポマイラ(自称ネイトの使い魔)

    「こうしてみると、この二日間で戦闘システムも大分良い感じになって来たな」

    画像
    ネイト(幻の星の精霊)

    「そうね。
    とは言え、まだ私たちのターンの処理ばかり作成してるからやることはいっぱいあるのだけどね」

    画像
    ポマイラ(自称ネイトの使い魔)

    「スキル効果処理は…作業時間8時間くらいか。
    やっぱ結構大変だったのか?」

    画像
    ネイト(幻の星の精霊)

    「そうね……AOE(範囲攻撃)の処理と、一部の特殊なスキルの処理周りを実装するのは大変だったわ。

    どうしても自分を攻撃するような動きになっちゃったりして……。

    ちょっと、スキル周りの処理はかなり複雑になってきたから、
    出来ればもう二度とコードを読みたくないわ……」

    画像
    ポマイラ(自称ネイトの使い魔)

    「とは言え、ここまで出来たらあとは殆どのスキルは使いまわしでいけるんだろ?」

    画像
    ネイト(幻の星の精霊)

    「まぁ、まだ状態異常の数も少ないし、思いつく限りでは他にも実装したい効果処理はあるのだけどね……。
    とはいえ、ベースのシステムとしては多分これで大丈夫……な、はず」

    画像
    ポマイラ(自称ネイトの使い魔)

    「おいおい、そんなに弱気で大丈夫か?」

    画像
    ネイト(幻の星の精霊)

    「うぅ……だって、本当に色んなスクリプトファイルを見なきゃいけないから、複雑すぎて自信ないのよ」

    画像
    ポマイラ(自称ネイトの使い魔)

    「まぁ、細かい調整は置いといて…
    今は動いたことを喜ぼうぜ!」

    画像
    ネイト(幻の星の精霊)

    「そうね!
    これでやっと次に進めるわ!」


    待機モーション作成

    画像
    ポマイラ(自称ネイトの使い魔)

    「そう言えば一つ気になったんだが、ユニットが動くようになったんだな」

    画像
    ネイト(幻の星の精霊)

    「そうなの! と言っても、まだ待機モーションだけだけどね。
    でも、これで少しはSRPGらしくなってきたんじゃないかしら?」

    画像
    ポマイラ(自称ネイトの使い魔)

    「確かに動きがあると臨場感が違うぜ!
    それで、どうやって作ったんだ?」

    画像
    ネイト(幻の星の精霊)

    「まず、Gemini先生にスプライトシート形式(アニメーションのコマ割り画像)で、待機モーションのドット絵を描いてもらったわ」


    画像
    画像
    ネイト(幻の星の精霊)

    「フレーム(コマ)数は多い方が滑らかに動くんだけど……
    そうすると、上手く意図した画像が生成されないパターンが多かったから、今回は3フレームにしたの」


    画像
    画像
    ネイト(幻の星の精霊)

    「そして、出力された画像を『GIMP(画像編集ソフト)』を使って細かく分割したわ」

    ■ ユニットを動かすスクリプト(UnitAnimator)

    // UnitAnimator.cs
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using TrainingGame.Battle;
    
    namespace TrainingGame.View
    {
        public class UnitAnimator : MonoBehaviour
        {
            [Header("フレーム間隔(秒)")]
            [SerializeField] private float _frameInterval = 0.3f;
    
            private SpriteRenderer _spriteRenderer;
            private Sprite[] _frames;
            private Coroutine _idleCoroutine;
    
            private void Awake()
            {
                _spriteRenderer = GetComponentInChildren<SpriteRenderer>();
            }
    
            /// <summary>
            /// BattleViewManager.SpawnUnit() からユニットデータを渡して初期化する。
            /// unitSprite をフレーム0として、idleFrames をその後に続ける。
            /// </summary>
            public void Initialize(UnitBaseStats stats)
            {
                var frameList = new List<Sprite>();
    
                if (stats.unitSprite != null)
                    frameList.Add(stats.unitSprite);
    
                if (stats.idleFrames != null)
                    frameList.AddRange(stats.idleFrames);
    
                _frames = frameList.ToArray();
    
                // フレーム0を初期表示
                if (_frames.Length > 0 && _spriteRenderer != null)
                    _spriteRenderer.sprite = _frames[0];
            }
    
            /// <summary>
            /// HandleTurnChanged から呼ぶ。このユニットがアクティブかどうかを渡す。
            /// </summary>
            public void SetActiveUnit(bool isActive)
            {
                if (isActive)
                    StartIdleAnimation();
                else
                    StopIdleAnimation();
            }
    
            private void StartIdleAnimation()
            {
                if (_idleCoroutine != null) StopCoroutine(_idleCoroutine);
                // フレームが1枚しかない場合はアニメーション不要
                if (_frames == null || _frames.Length <= 1) return;
                _idleCoroutine = StartCoroutine(PlayIdle());
            }
    
            private void StopIdleAnimation()
            {
                if (_idleCoroutine != null)
                {
                    StopCoroutine(_idleCoroutine);
                    _idleCoroutine = null;
                }
                // フレーム0(通常立ち)に戻す
                if (_frames != null && _frames.Length > 0 && _spriteRenderer != null)
                    _spriteRenderer.sprite = _frames[0];
            }
    
            private IEnumerator PlayIdle()
            {
                int index = 0;
                while (true)
                {
                    _spriteRenderer.sprite = _frames[index];
                    index = (index + 1) % _frames.Length;
                    yield return new WaitForSeconds(_frameInterval);
                }
            }
        }
    }
    画像
    ネイト(幻の星の精霊)

    「あとは、ユニットの差分(コマごとの画像)をこの子に登録すれば、
    行動可能なユニットの待機モーションアニメーションは完成よ!」


    private IEnumerator PlayIdle()
            {
                int index = 0;
                while (true)
                {
                    _spriteRenderer.sprite = _frames[index];
                    index = (index + 1) % _frames.Length;
                    yield return new WaitForSeconds(_frameInterval);
                }
            }
    画像
    ネイト(幻の星の精霊)

    「特にこの部分で、フレームごとにスプライトを順番にコマ送り表示させているの」

    画像
    ポマイラ(自称ネイトの使い魔)

    「なるほどなぁ。 ちなみに、攻撃とか他のモーションも作るのか?」

    画像
    ネイト(幻の星の精霊)

    「作りたいけど……今は後回しにしてるわ。
    だって、モーションを一つ増やすごとに、全ユニット分の差分画像を作成する必要があるでしょ?
    今の開発スケジュールじゃ、時間的に少し厳しいのよ」

    画像
    ネイト(幻の星の精霊)

    「とはいえ! 戦闘モーションはSRPGの華よね!
    まずは戦闘システムの構築を完了させて、少し落ち着いたらスキルのエフェクトといった演出面と合わせて必ず検討してみるわ!」


    最後に

    かくして、進撃の開発16日目は幕を閉じた。


    読者の皆様から寄せられた大切なアイディアは、
    試行錯誤の末、無事に「スキルピックシステム」として実装され、ネイトの新たな力となった。


    8時間にも及ぶコードとの死闘の末、
    範囲攻撃や特殊効果といった複雑な処理を乗り越え、ついに戦闘の要であるスキルシステムが産声を上げたのである。

    さらに、ドット絵に命を吹き込むことで、戦場に立つキャラクターたちは「待機モーション」という確かな鼓動を手に入れた。


    止まっていた画面の時間が動き出し、
    SRPGとしての輪郭がまた一つ、色濃く浮かび上がってきたのだ。


    だが、安堵している暇はない。


    企画のタイムリミットは、残りわずか2週間。
    次なる試練は敵ユニットの思考処理か、それとも立ちはだかる未知のバグか。


    ネイトたちの往く手には、まだまだ険しい開発の道が続いている……。


    果たして、期限内にゲームは完成するのか!?


    次回も、どうぞ乞うご期待!

    #プロジェクトNeith #HorizonNotes #ゲーム制作 #個人開発 #Unity #開発日記 #読者参加型 #毎日note

     
     
    読者参加型でゲームを作っている、まる。です。 UE5でオリジナルのカードゲームRPG『HorizonNotes』を開発してます 育成SRPG『ネイト育成計画』体験版、無料公開中です! ■公式サイト https://horizonnotes.netlify.app/

    あなたへのおすすめ