RobKnight_icon Image

Rob Knight

RobKnight_Preview Image
RobKnight_Play Image
RobKnight_Play2 Image
RobKnight_Play3 Image
RobKnight_Play4 Image
RobKnight_Play6 Image
RobKnight_Play5 Image
Slide
PlayPlay
RobKnight_Preview
RobKnight_Play
RobKnight_Play2
RobKnight_Play3
RobKnight_Play4
RobKnight_Play6
RobKnight_Play5
previous arrow
next arrow
Slide
RobKnight_Preview
RobKnight_Play
RobKnight_Play2
RobKnight_Play3
RobKnight_Play4
RobKnight_Play6
RobKnight_Play5
previous arrow
next arrow

憑依の力を使い、謎解きと立ちはだかるボスを打ち倒せ。
憑依3Dアクションゲーム。

プラットフォーム:PC, Android, Web

詳細は公式サイトをチェック
ダウンロードやWeb版でのプレイは公式サイトから行えます。

開発環境Unity、SvelteKit
開発期間約2ヶ月間
担当箇所エネミー、各シーン、ツール作成、動画作成、サイト作成

初めてのUnity開発でしたが、私が普段ゲーム開発に使用しているDXライブラリをC++で使うような仕組みで開発するのは面白くないと思い、色々なパフォーマン最適化かつ開発の効率化に挑戦してみました。

エネミーの行動パターン

エネミーの行動は、待機、徘徊、追跡、攻撃の4種類に大別されます。これらの行動パターンを効率的に管理するために、スクリプタブルオブジェクトを用いて実装しました。このアプローチにより、エディタ上で容易にパラメータを調整できるだけでなく、エネミーの数だけインスタンスを作成する必要がない為、メモリ使用量の削減にも繋がります。また、各行動パターンもスクリプタブルオブジェクトで実装し、個別にカスタマイズ可能とすることで、ボスエネミーも含めたエネミーに待機、徘徊、追跡、攻撃の4種類に大別された行動が共有して利用可能となり、柔軟性とパフォーマンスの向上を両立させています。

特定のポイント(座標)を徘徊する「PatrolPoint」という徘徊方法では、Vector3の座標を手入力で入力する手間を省く為に、選択したオブジェクトの座標を自動で入力してくれるエディタ拡張スクリプトを作成することで、作業効率の向上に努めました。

さらに、エネミーのHPや攻撃力などのパラメータもスクリプタブルオブジェクトとして管理しています。これにより、各エネミーのパラメータの変更が容易になり、デバッグやパラメータ調整の効率が大幅に向上しました。

EnemyBehavior.cs
using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using Random = UnityEngine.Random;


[CreateAssetMenu(menuName = "EnemyBehavior")]
public class EnemyBehavior : ScriptableObject
{


    public bool UseAction = false;  //アクション配列に基づいて行動する? 
    private int ActionNum;      //現在の行動配列番号(Action配列を使う場合のみ使用)
    private EnemyAction NowAction;  //現在の行動        (Action配列を使う場合のみ使用)


    public List<EnemyAction> actions;
    public EnemyAction attackAction;
    public EnemyAction idleAction;
    public List<EnemyAction> wanderAction;
    public EnemyAction chaseAction;
    public float idleFrequency = 0.1f; // アイドル状態になる確率を10%に設定
    public float idleTime = 2f; // アイドル状態の持続時間
    private bool isIdle = false;
    private float idleTimer = 0f; // アイドル用のタイマー
    public float attackRange = 2f; // 攻撃範囲
    private EnemyState currentState;
    private EnemyAction _selectedAction;
    private event Action _onDamageHandler;  // 攻撃受けたときに発生するイベントの経由イベント


    void OnEnable()
    {
        //行動の初期化
        ActionNum = 0;
        for (int i = 0; i < actions.Count; i++) 
        {
            actions[ActionNum].ActionTime = 0;
            actions[ActionNum].IsComplete = false;
        }
    }


    //ビヘイビアの初期化
    public void Initialize(EnemyController controller)
    {
        _onDamageHandler = () => TriggerCounterAttack(controller);
        controller.OnDamage += _onDamageHandler;
        
        //行動の初期化
        ActionNum = 0;
        for (int i = 0; i < actions.Count; i++)
        {
            actions[ActionNum].ActionTime = 0;
            actions[ActionNum].IsComplete = false;
        }
    }
    
    public void Cleanup(EnemyController controller)
    {
        controller.OnDamage -= _onDamageHandler;
    }


    private enum EnemyState
    {
        Idle,
        Wander,
        Chase,
        Attack
    }


    //プレイヤーの方向を向く
    private void TriggerCounterAttack(EnemyController controller)
    {
        //攻撃動作中でない場合にプレイヤーの方を向く
        if (!controller.GetAttacking())
        {
            controller.transform.LookAt(controller.player); // プレイヤーの方向を向く
            //attackAction.Act(controller); // 攻撃行動に移る
            Vector3 myAngle = controller.transform.eulerAngles;
            myAngle.x = 0;
            controller.transform.eulerAngles = myAngle;
        }
    }


    public void PerformActions(EnemyController controller)
    {


        float distanceToPlayer = Vector3.Distance(controller.transform.position, controller.player.position);


        //UseActionをチェックする
        if (UseAction)
        {
            //アクション配列に基づいて行動する
            actions[ActionNum].Act(controller);
            actions[ActionNum].ActionTime += Time.fixedDeltaTime;


            //行動終了時、新たな行動をセットする
            if (actions[ActionNum].IsComplete)
            {
                if (++ActionNum >= actions.Count) ActionNum = 0;


                actions[ActionNum].IsComplete = false;
                actions[ActionNum].ActionTime = 0;
            }


        }
        else
        {
            //アクション配列に基づかない行動
            if (controller.IsPlayerFound())
            {
                if (distanceToPlayer <= attackRange)
                {
                    attackAction.Act(controller);
                    currentState = EnemyState.Attack;

                }
                else
                {
                    chaseAction.Act(controller);
                    currentState = EnemyState.Chase;
                }


                // プレイヤー発見時はアイドルタイマーをリセット
                idleTimer = 0f;
                isIdle = false;
            }
            else
            {
                if (isIdle)
                {
                    if (idleTimer < idleTime)
                    {
                        idleAction.Act(controller);
                        currentState = EnemyState.Idle;
                        idleTimer += Time.deltaTime; // タイマーを更新
                    }
                    else
                    {
                        // アイドル時間が経過したら、状態を切り替え
                        isIdle = false;
                        idleTimer = 0f;
                    }
                }
                else
                {
                    wanderAction[0].Act(controller);
                    currentState = EnemyState.Wander;


                    // ランダムな確率でアイドル状態に切り替える
                    if (Random.value < idleFrequency)
                    {
                        isIdle = true;
                    }
                }
            }
        }
       
    }
PatrolPointWander.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "EnemyActions/PatrolPointWanderAction")]
public class PatrolPointWander : EnemyAction
{
    public float moveSpeed = 2f;
    public Vector3[] patrolPoints;
    private int _nextPatrolPointIndex = 0;


    public override void Act(EnemyController controller)
    {
        Patrol(controller);

        float detectionRadius = 5.0f;
        float avoidanceStrength = 5.0f;
        

    }

    private void Patrol(EnemyController controller)
    {
        Rigidbody rb = controller.GetComponent<Rigidbody>();
        if (rb != null)
        {   
            // 現在再生中のアニメーションの状態を取得
            AnimatorStateInfo stateInfo = controller.animator.GetCurrentAnimatorStateInfo(0);
            controller.animator.SetFloat("Speed", moveSpeed);
            
            // 再生しているアニメーションにWalkタグが付いている場合
            if (stateInfo.IsTag("Move"))
            {
                // パトロールポイントが設定されているか確認
                if (patrolPoints != null && patrolPoints.Length > 0)
                {

                    // 次のパトロールポイントへ移動
                    Vector3 nextPatrolPoint = patrolPoints[_nextPatrolPointIndex];
                    if (MoveTowardsPoint(controller, rb, nextPatrolPoint, moveSpeed))
                    {

                        // パトロールポイントに到着したら次のポイントに移動
                        _nextPatrolPointIndex = (_nextPatrolPointIndex + 1) % patrolPoints.Length;
                    }
                }
            }
        }
    }

    // 指定されたポイントに移動するためのヘルパーメソッド
    private bool MoveTowardsPoint(EnemyController controller, Rigidbody rb, Vector3 targetPoint, float speed)
    {
        // targetPoint の Y 座標を現在の rb.position の Y 座標に設定することで無視
        targetPoint.y = rb.position.y;
        
        Vector3 direction = (targetPoint - rb.position).normalized;
        // エネミーがプレイヤーの方向を向く(Y軸の回転は除く)
        Quaternion lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z));
        controller.transform.rotation = Quaternion.Slerp(controller.transform.rotation, lookRotation, Time.deltaTime * 5f);
        
        float distanceToTarget = Vector3.Distance(rb.position, targetPoint);
        if (distanceToTarget > 0.1f)
        {
            Vector3 newPosition = rb.position + direction * (speed * Time.fixedDeltaTime);
            rb.MovePosition(newPosition);
            return false; // まだ目的地に到着していない
        }
        return true; // 目的地に到着した
    }
}
EnemyData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "NewEnemyData", menuName = "Enemy Data", order = 51)]
public class EnemyData : ScriptableObject
{
    public string enemyName;

    //敵としての体力
    public int hp;
    public int maxHp;

    //プレイヤー時の体力
    public int Poshp;

    public int attackPower;
    public GameObject modelPrefab;
    // 技
    public Ability[] abilities;


    public EnemyData(string name, int health, int maxHealth, int attack, GameObject model)
    {
        enemyName = name;
        hp = health;
        maxHp = maxHealth;
        attackPower = attack;
        modelPrefab = model;
    }
}