Unity の学習記録 - モデルとアニメーション

Unity の学習記録 - モデルとアニメーション

序文

この記事は、中山大学ソフトウェア工学部の 3D ゲーム プログラミングとデザインの 2020 年度クラスの課題 7 です。

プログラミングの質問: インテリジェントパトロール

1. 研究参考資料

教師が授業で教えたことを除き、この課題のコードと操作は主に[Fu氏/Unity指導] DarkSouls Reproduction Classic Tutorial#Season 1_ bilibiliを参照しています。

モデル アクションのリソースは、Fuwang クラスキャラクター モデルとアニメーション (weiyun.com) で教師が提供したチュートリアルからも取得されます。

2.基本的な紹介

(1)unityアニメーション1

Unity には豊富で複雑なアニメーション システム (「Mecanim」と呼ばれることもあります) があります。このシステムには次の機能があります。

  • オブジェクト、キャラクター、プロパティなど、Unity のすべての要素にシンプルなワークフローとアニメーション設定を提供します。
  • インポートされたアニメーション クリップと Unity 内で作成されたアニメーションをサポート
  • ヒューマノイド アニメーション リターゲット - あるキャラクター モデルから別のキャラクター モデルにアニメーションを適用する機能。
  • アニメーション クリップを配置するためのワークフローが簡素化されました。
  • アニメーション クリップだけでなく、アニメーション クリップ間のトランジションやインタラクションも簡単にプレビューできます。その結果、アニメーターとエンジニアはより独立して作業できるようになり、アニメーターはアニメーションをゲーム コードに組み込む前にプロトタイプを作成してプレビューできるようになります。
  • アニメーション間の複雑な相互作用を管理するビジュアル プログラミング ツールを提供します。
  • さまざまなボディパーツをさまざまなロジックでアニメーション化します。
  • レイヤ化およびマスキング機能
(2) パブリッシュ/サブスクライブ モード2

パブリッシュ/サブスクライブ パターン (Publish Subscribe パターン) は、デザイン パターンの動作パターン (Behavioral Patterns) に属します。

ソフトウェア アーキテクチャでは、パブリッシュ/サブスクライブ は、メッセージの送信者 (パブリッシャーと呼ばれる) がメッセージを送信しないメッセージング パラダイムです。メッセージは特定の受信者 (サブスクライバと呼ばれる) に直接送信されますが、メッセージ トピックをサブスクライブするサブスクライバがメッセージを消費できるように、メッセージ チャネルを通じてメッセージをブロードキャストします。

パブリッシャー/サブスクライバー モデル最大の特徴は、疎結合の実現です。つまり、パブリッシャーにメッセージをパブリッシュさせ、サブスクライブ ユーザーは、2 つの別々のシステムを接続する方法を探すのではなく、メッセージを受け入れます。もちろん、この疎結合はパブリッシャー/サブスクライバー モデルの最大の欠点でもあります。中間エージェントが必要となり、システムの複雑さが増大するからです。さらに、発行者は、発行されたメッセージが各加入者に受信されたかどうかをリアルタイムで知ることができないため、システムの不確実性が増大します。

3. 質問要件

インテリジェントパトロール

  • 提出要件:

  • ゲームデザインの要件:

    • マップと 1,000 のパトロールを作成します (アニメーションを使用)。

    • 各パトロールは3~5辺の凸多角形を歩き、位置データは相対アドレスとなります。つまり、次の目標位置を決定するたびに、現在の位置を原点として使用して計算します。

    • パトロールが障害物に衝突すると、次のポイントがターゲットとして自動的に選択されます。

    • パトロールが設定範囲内でプレイヤーを検出すると、自動的にプレイヤーを追跡します。

    • プレイヤーのターゲットを失った後もパトロールを続けます。

    • 得点: プレイヤーがパトロールを投げ捨てるたびに 1 ポイントが獲得され、プレイヤーがパトロールと衝突するとゲームが終了します。

  • プログラミング要件:

    • メッセージはサブスクリプション モードとパブリッシュ モードを使用して送信する必要があります
      • 件名: 失われた目標について
      • 出版社: ?
      • 購読者: ?
      • ファクトリーモードではパトロール兵士が生成される
  • 親切なヒント 1: 3 ~ 5 辺の凸多角形を生成する

    • ランダムに長方形を生成
    • 長方形の各辺で点をランダムに見つけて、3 ~ 4 個の多角形を取得します
    • 5?
  • フレンドリーなヒント 2: 以前のブログを参照して、独自の新しいゲームプレイを作成してください

4. 動作とコードの詳細説明

(1)アニメーターコントローラー制作
1) アニメーションをインポートする

このキャラクターアニメーションでは主に以下のアクションを使用します。

それぞれ:スタンバイ、ジャンプバック、落下、ジャンプ、転がり、走る、歩く

画像の説明を追加してください

2) アニメーション状態図の描画

以下の変更に従って状態図を描画します

画像の説明を追加してください

3) アニメーションブレンドツリーを設定する

アニメーション ブレンディング ツリーは、アニメーション ステート マシン内の状態であり、複数のアニメーションのブレンドであり、複数の同様のアニメーションのスムーズな遷移を実現できます。

地面を右クリックし、[Create new BlendTree in State] をクリックして、アニメーション化されたブレンド ツリーを作成します。

画像の説明を追加してください

BlendTree をセットアップし、いくつかの値を変更し、地上にスタンバイ、歩行、ランニングの 3 つのアニメーションを追加します。

画像の説明を追加してください

4)変更点

次のように転送制御変数を追加します。

画像の説明を追加してください

同時に、次のように、基底状態からロール状態への遷移を例として、各遷移に対応する制御条件変数を追加します。

画像の説明を追加してください

5)FSMコード

アニメーション ステート マシンでは、対応するアクションによってステート マシンの状態が変化し、対応する変化も引き起こされます。コードをステートに直接マウントすると、プロジェクトが複雑になり、デバッグが困難になります。このとき、パブリッシュ/サブスクライブ モードを使用して、ステート マシンの変更から対応する情報を取得し、ブロードキャストすることができます。

メッセージ ブロードキャストとステート マシンの初期化のための 4 つの関数を作成しましたFSMClearSignals(),FSMOnEnter(),FSMOnExit(),FSMOnUpdate()

以下はFSMClearSignals() を初期化するためのコードです。

public class FSMClearSignals : StateMachineBehaviour {
    public string[] ClearAtEnter;
    public string[] ClearAtExit;

    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var signal in ClearAtEnter) {
            animator.ResetTrigger(signal);
        }
    }

    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var signal in ClearAtExit) {
            animator.ResetTrigger(signal);
        }
    }
}

以下は、 状態に入るコードFSMOnEnter() で、状態に入るときにブロードキャストされます。FSMOnExit(),FSMOnUpdate() は基本的にこのコードと同じであり、ここではリリースされません

public class FSMOnEnter : StateMachineBehaviour {
    public string[] onEnterMessages;

    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
        foreach (var msg in onEnterMessages) {
            animator.gameObject.SendMessageUpwards(msg);
        }
    }
}

対応する状態でコードをマウントします。ここでは例としてアースのみを取り上げます。

画像の説明を追加してください

6) 備考

モデル アニメーションをダウンロードした後、このジョブに適応させるために、いくつかのアクション設定を変更する必要があります (主にポーズにベイク処理オプションのチェックをオンにします)。一部のアクションの再生時にオブジェクトを回転したり移動したりする必要はありません。上記の調整を行います。実際の状況に応じて設定を行うだけで、7つのアクションに従うだけです

画像の説明を追加してください

(2) プレハブ製作

再次声明:

これには、プレーヤーのコントロール入力、着地検出、三人称視点の実装など、より複雑なコードと操作が含まれます。これらは基本的にすべて [Fu先生/Unity指導] DarkSouls再現クラシックチュートリアル#シーズン1_bilibili

1)プレイヤー

次の階層関係に従って Player オブジェクトを作成します。ybot のみがモデルで、残りは空です

画像の説明を追加してください

三人称の実現

カメラを制御するためにCameraConrtoller()を書き込みます。コードは次のとおりです:

public class CameraConrtoller : MonoBehaviour {
    public PlayerInput pi;
    public float horizontalSpeed = 100f;
    public float verticalSpeed = 80f;
    public float cameraDampValue = 0.5f;

    private GameObject playerHandle;
    private GameObject cameraHandle;
    private float tempEulerX;
    private GameObject model;
    protected GameObject mycamera;

    private Vector3 cameraDampVelocity;
    

    void Awake() {
        cameraHandle = transform.parent.gameObject;
        playerHandle = cameraHandle.transform.parent.gameObject;
        model = playerHandle.GetComponent<ActorController>().model;
        mycamera = Camera.main.gameObject;
        tempEulerX = 20f;
    }

    // Update is called once per frame
    void FixedUpdate() {
        Vector3 tempModelEuler = model.transform.eulerAngles;
        playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);
        tempEulerX -= pi.Jup * verticalSpeed * Time.fixedDeltaTime;
        tempEulerX = Mathf.Clamp(tempEulerX, -35, 30);
        cameraHandle.transform.localEulerAngles = new Vector3(tempEulerX, 0, 0);
        model.transform.eulerAngles = tempModelEuler;

        mycamera.transform.position = Vector3.SmoothDamp(
            mycamera.transform.position, transform.position, 
            ref cameraDampVelocity, cameraDampValue);
        mycamera.transform.eulerAngles = transform.eulerAngles;
    }
}

このコードを CameraPos にマウントし、Pi をプレーヤーとして設定します

画像の説明を追加してください

フロアモニタリング

着地検出用にOnGroundSensor()を記述します。ロジックは、キャラクターと同じサイズの CapsuleCollider (カプセル コライダー) を追加し、コライダーが地面レイヤーと衝突するかどうかを検出することです。コードは次のとおりです:

public class OnGroundSensor : MonoBehaviour
{
    public CapsuleCollider capcol;
    public float offset = 0.1f;
    private Vector3 point1;
    private Vector3 point2;
    private float radius;
    void Awake()
    {
        radius = capcol.radius;
    }
    void FixedUpdate()
    {
        point1 = transform.position + transform.up * (radius - offset);
        point2 = transform.position + transform.up * (capcol.height - offset) - transform.up * radius;
        Collider[] outputCols = Physics.OverlapCapsule(point1, point2, radius, LayerMask.GetMask("Ground"));
        if (outputCols.Length != 0)
        {
            SendMessageUpwards("OnGround");
        }
        else
        {
            SendMessageUpwards("NotOnGround");
        }
    }
}

このコードはセンサーにマウントされ、capcol も Player に設定されます

画像の説明を追加してください

プレーヤー制御モジュール

コードは次のとおりです。

public class PlayerInput : MonoBehaviour
{
    [Header("---- KeyCode Settings ----")]
    public string keyUp = "w";
    public string keyDown = "s";
    public string keyLeft = "a";
    public string keyRight = "d";

    public string keyA = "left shift";
    public string keyB = "space";
    public string keyC = "k";
    public string keyD;

    public string keyJUp = "up";
    public string keyJDown = "down";
    public string keyJLeft = "left";
    public string keyJRight = "right";

    [Header("---- Output Settings ----")]
    public float Dup;
    public float Dright;
    public float Dmag;//方向
    public Vector3 Dvec;//速度

    public float Jup;
    public float Jright;

    //1.pressing signal
    public bool run;
    public bool jump;
    private bool lastJump;
    //2.trigger signal
    //3.double signal

    [Header("---- Other Settings ----")]
    public bool inputEnabled = true;

    private float targetDup;
    private float targetDright;
    private float velocityDup;
    private float velocityDright;

    void Start() { }

    void Update()
    {
        Jup = (Input.GetKey(keyJUp)) ? 1.0f : 0 - (Input.GetKey(keyJDown) ? 1.0f : 0);
        Jright = (Input.GetKey(keyJRight)) ? 1.0f : 0 - (Input.GetKey(keyJLeft) ? 1.0f : 0);

        targetDup = (Input.GetKey(keyUp) ? 1.0f : 0) - (Input.GetKey(keyDown) ? 1.0f : 0);
        targetDright = (Input.GetKey(keyRight) ? 1.0f : 0) - (Input.GetKey(keyLeft) ? 1.0f : 0);

        if (!inputEnabled)
        {
            targetDup = 0;
            targetDright = 0;
        }
        //平滑变动
        Dup = Mathf.SmoothDamp(Dup, targetDup, ref velocityDup, 0.1f);
        Dright = Mathf.SmoothDamp(Dright, targetDright, ref velocityDright, 0.1f);

        /*矩形坐标转圆坐标*/
        Vector2 tempDAxis = SquareToCircle(new Vector2(Dup, Dright));
        float Dup2 = tempDAxis.x;
        float Dright2 = tempDAxis.y;

        Dmag = Mathf.Sqrt((Dup2 * Dup2) + (Dright2 * Dright2));
        Dvec = Dright * transform.right + Dup * transform.forward;
        run = Input.GetKey(keyA);

        /*跳跃*/
        bool newJump = Input.GetKey(keyB);
        lastJump = jump;
        if (lastJump == false && newJump == true)
        {
            jump = true;
        }
        else
        {
            jump = false;
        }
    }

    /*矩形坐标转圆坐标*/
    private Vector2 SquareToCircle(Vector2 input)
    {
        Vector2 output = Vector2.zero;
        output.x = input.x * Mathf.Sqrt(1 - (input.y * input.y) / 2.0f);
        output.y = input.y * Mathf.Sqrt(1 - (input.x * input.x) / 2.0f);
        return output;
    }
}
キャラクター制御モジュール

コードは次のとおりです。

public class ActorController : MonoBehaviour
{
    public GameObject model;
    public PlayerInput pi;
    public float walkSpeed = 1.5f;
    public float runMultiplier = 2.7f;
    public float jumpVelocity = 4f;
    public float rollVelocity = 1f;

    [SerializeField]
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private Vector3 thrustVec; // 跳跃冲量

    private bool lockPlanar = false; // 跳跃时锁死平面移动向量

    void Awake()
    {
        pi = GetComponent<PlayerInput>();
        anim = model.GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
    }

    //刷新每秒60次
    void Update()
    {
        //修改动画混合树
        /*1.从走路到跑步没有过渡*/
        /*anim.SetFloat("forward", pi.Dmag * (pi.run ? 2.0f : 1.0f));*/
        /*2.使用Lerp加权平均解决*/
        float targetRunMulti = pi.run ? 2.0f : 1.0f;
        anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"), targetRunMulti, 0.3f));
        //播放翻滚动画
        if (rigid.velocity.magnitude > 1.0f)
        {
            anim.SetTrigger("roll");
        }
        //播放跳跃动画
        if (pi.jump)
        {
            anim.SetTrigger("jump");
        }
        //转向
        if (pi.Dmag > 0.01f)
        {
            /*1.旋转太快没有补帧*/
            /*model.transform.forward = pi.Dvec;*/
            /*2.使用Slerp内插值解决*/
            Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.Dvec, 0.2f);
            model.transform.forward = targetForward;
        }
        if (!lockPlanar)
        {
            //保存供物理引擎使用
            planarVec = pi.Dmag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
        }

    }

    //物理引擎每秒50次
    private void FixedUpdate()
    {
        //Time.fixedDeltaTime 50/s
        //1.修改位置
        //rigid.position += movingVec * Time.fixedDeltaTime;
        //2.修改速度
        rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z) + thrustVec;
        //一帧
        thrustVec = Vector3.zero;
    }

    public void OnJumpEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
        thrustVec = new Vector3(0, jumpVelocity, 0);
    }

    public void OnRollEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    public void OnRollUpdate()
    {
        thrustVec = model.transform.forward * anim.GetFloat("rollVelocity") * 1.0f;
    }

    public void OnGround()
    {
        anim.SetBool("OnGround", true);
    }

    public void NotOnGround()
    {
        anim.SetBool("OnGround", false);
    }

    public void OnGroundEnter()
    {
        pi.inputEnabled = true;
        lockPlanar = false;
    }

    public void OnFallEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    public void OnJabEnter()
    {
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    public void OnJabUpdate()
    {
        thrustVec = model.transform.forward * anim.GetFloat("jabVelocity") * 1.4f;
    }
}
衝突イベントの実装

コードは次のとおりです。

public class PlayerCollide : MonoBehaviour {
    void OnCollisionEnter(Collision other) {
        //当玩家与侦察兵相撞
        if (other.gameObject.tag == "Guard") {
            Singleton<GameEventManager>.Instance.PlayerGameover();
        }
    }
}
実装はコンポーネントに関連しています

上記 3 つのコードはすべて Player に実装されていますが、同時に次のように剛体コンポーネントとカプセル衝突ボディを Player に追加する必要があります。

画像の説明を追加してください

同時に、次のように、以前に作成した Animator Controller を ybot の Animator コンポーネントに追加する必要があります。

画像の説明を追加してください

2)ガード

次の階層関係に従って Guard オブジェクトを作成します。ここで、ybot のみがモデルであり、Guard の親オブジェクトは空です。

画像の説明を追加してください

パトロールデータモジュール

コードは次のとおりです。主に、パトロール エンティティの後続の必要なデータを設定します。

public class GuardData : MonoBehaviour {
    public GameObject model;
    public float walkSpeed = 1.2f;
    public float runSpeed = 2.5f;
    public int sign;                      //标志巡逻兵在哪一块区域
    public bool isFollow = false;         //是否跟随玩家
    public int playerSign = -1;           //当前玩家所在区域标志
    public Vector3 start_position;        //当前巡逻兵初始位置   

    [SerializeField]
    private Animator anim;
    private Rigidbody rigid;

    void Awake() {
        anim = model.GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
    }

    public void OnGround() {
        anim.SetBool("OnGround", true);
    }
    public void OnGroundEnter() {
        
    }
}
実装はコンポーネントに関連しています

上記のコードは Guard にマウントする必要があり、次のように剛体コンポーネントとカプセル衝突ボディを Guard に追加する必要があります。

画像の説明を追加してください

注: ここでの ybot の Animator コンポーネントは、以前に作成した Animator Controller を追加する必要はありません。

3)平面

Plane は、プレイヤーとパトロールがこのプロジェクトで行動するためのプラットフォームであり、次のようにプレハブ化されています。

このうち、Planeの親オブジェクト、Sensor、TriggerはすべてEmpty、Planeの子オブジェクトは地面、Wallは別の正面です

画像の説明を追加してください

画像の説明を追加してください

衝突イベントの実装

プレーヤーが地面または壁に衝突した後、プレーヤーが位置するエリアが送信されます。コードは次のとおりです。

public class AreaCollide : MonoBehaviour {
    public int sign = 0;
    private FirstSceneController sceneController;

    private void Start() {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }

    void OnTriggerEnter(Collider collider) {
        if (collider.gameObject.tag == "Player") {
            sceneController.playerSign = sign;
        }
    }
}
実装はコンポーネントに関連しています

Plane 親オブジェクトを確立するとき、Player で着陸監視機能を実装するには、Plane の Layer を Ground に設定する必要があります。

画像の説明を追加してください

各トリガーに対応するサイズのコリジョン ボディを設定し、AreaCollide()関数をマウントする必要があります。

画像の説明を追加してください

(3) コードの実装
1) アクションの分離

は、SSAction() アクション基本クラス、 SSActionManager() アクション管理クラスを含む、前の割り当てのアクション分離と基本的に同じです。ここでリリースされます。

パトロールを実装するアクション管理クラスはGuardActionManager()で、パトロールとプレイヤーの追跡という 2 つのアクションを実装します。コードは次のとおりです:

public class GuardActionManager : SSActionManager, ISSActionCallback {
    private GuardPatrolAction patrol;
    private GameObject player;
    public void GuardPatrol(GameObject guard, GameObject _player) {
        player = _player;
        patrol = GuardPatrolAction.GetSSAction(guard.transform.position);
        this.RunAction(guard, patrol, this);
    }

    public void SSActionEvent(
        SSAction source, SSActionEventType events = SSActionEventType.Competeted,
        int intParam = 0, GameObject objectParam = null) {
        if (intParam == 0) {
            //追逐
            GuardFollowAction follow = GuardFollowAction.GetSSAction(player);
            this.RunAction(objectParam, follow, this);
        } else {
            //巡逻
            GuardPatrolAction move = GuardPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<GuardData>().start_position);
            this.RunAction(objectParam, move, this);
            Singleton<GameEventManager>.Instance.PlayerEscape();
        }
    }
}
2) パトロールアーセナルモード

パトロールをバッチ生成するには、ファクトリ モードを使用します。マウスを使用して空飛ぶ円盤をプレイすることについてはレッスンですでに学習したため、コードの実装はここでは紹介しません。コードのみが次のようにリリースされます。

public class GuardFactory : MonoBehaviour {
    private GameObject guard = null;                               //巡逻兵
    private List<GameObject> used = new List<GameObject>();        //正在使用的巡逻兵列表
    private Vector3[] vec = new Vector3[9];                        //每个巡逻兵的初始位置

    public List<GameObject> GetPatrols() {
        int[] pos_x = { -6, 4, 13 };
        int[] pos_z = { -4, 6, -13 };
        int index = 0;
        for(int i=0;i < 3;i++) {
            for(int j=0;j < 3;j++) {
                vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
                index++;
            }
        }
        for(int i = 0; i < 8; i++) {
            guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
            guard.transform.position = vec[i];
            guard.GetComponent<GuardData>().sign = i + 1;
            guard.GetComponent<GuardData>().start_position = vec[i];
            guard.GetComponent<Animator>().SetFloat("forward", 1);
            used.Add(guard);
        }   
        return used;
    }
}
3) パトロール移動戦略

パトロールとチェイスの2つのパートに分かれています

パトロールとは、長方形の範囲内で繰り返し移動することを指し、パトロールコードは次のとおりです。

public class GuardPatrolAction : SSAction {
    private enum Dirction { EAST, NORTH, WEST, SOUTH };
    private float pos_x, pos_z;                 
    private float move_length;                 
    private bool move_sign = true;              
    private Dirction dirction = Dirction.EAST;  

    private GuardData data;
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private GuardPatrolAction() { }

    public override void Start() {
        data = gameobject.GetComponent<GuardData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();
        //播放走路动画
        anim.SetFloat("forward", 1.0f);
    }
    public static GuardPatrolAction GetSSAction(Vector3 location) {
        GuardPatrolAction action = CreateInstance<GuardPatrolAction>();
        action.pos_x = location.x;
        action.pos_z = location.z;
        //设定移动矩形的边长
        action.move_length = Random.Range(5, 6);
        return action;
    }

    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * data.walkSpeed;
    }

    public override void FixedUpdate() {
        //巡逻
        Gopatrol();
        //玩家进入该区域,巡逻结束,开始追逐
        if (data.playerSign == data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
        }
    }

    void Gopatrol() {
        if (move_sign) {
            //不需要转向则设定一个目的地,按照矩形移动
            switch (dirction) {
                case Dirction.EAST:
                    pos_x -= move_length;
                    break;
                case Dirction.NORTH:
                    pos_z += move_length;
                    break;
                case Dirction.WEST:
                    pos_x += move_length;
                    break;
                case Dirction.SOUTH:
                    pos_z -= move_length;
                    break;
            }
            move_sign = false;
        }
        this.transform.LookAt(new Vector3(pos_x, 0, pos_z));
        float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));

        if (distance > 0.9) {
            rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
        } else {
            dirction = dirction + 1;
            if(dirction > Dirction.SOUTH) {
                dirction = Dirction.EAST;
            }
            move_sign = true;
        }
    }
}

追跡はプレイヤーが巡回エリアに入ると発生し、プレイヤーが入った後はプレイヤーに向かって移動し、プレイヤーが巡回エリアから出ると巡回します。

public class GuardFollowAction : SSAction {
    private GameObject player;        
    private GuardData data;
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private float speed;

    private GuardFollowAction() {}

    public override void Start() {
        data = gameobject.GetComponent<GuardData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();
        speed = data.runSpeed;
        anim.SetFloat("forward", 2.0f);
    }

    public static GuardFollowAction GetSSAction(GameObject player) {
        GuardFollowAction action = CreateInstance<GuardFollowAction>();
        action.player = player;
        return action;
    }

    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * speed;
    }

    public override void FixedUpdate() {
        transform.LookAt(player.transform.position);
        rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
        
        //如果玩家脱离该区域则继续巡逻
        if (data.playerSign != data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 1, this.gameobject);
        }
    }
}
4) パブリッシュ/サブスクライブモデル

はメッセージをグローバルに公開するためにGameEventManager() を作成し、メッセージの受信側部分は に実装されています。FirstSceneController()

public class GameEventManager : MonoBehaviour {
    public delegate void ScoreEvent();
    public static event ScoreEvent ScoreChange;
    
    public delegate void GameoverEvent();
    public static event GameoverEvent GameoverChange;

    public void PlayerEscape() {
        if (ScoreChange != null) {
            ScoreChange();
        }
    }

    public void PlayerGameover(){
        if (GameoverChange != null) {
            GameoverChange();
        }
    }
}
5)その他のコード

次のようなプロジェクトの他のコード:SSDirector(),Singleton(),UserGUI(),Interface() は今後リリースされません。プロジェクトを表示するには記事の最後に移動してください。

デモ

今回は録画した動画が大きいため、GIFは作成しません。デモ ビデオはステーション bSmart Patrol-Bilibili-bilibili に送信されました。

コードの場所

コードとドキュメントはhw7 · XiaoChen04_3/3D_Computer_Game_Programming - gitee にアップロードされました。


  1. アニメーション システムの概要 - Unity マニュアル ↩︎

  2. デザイン パターン パブリッシュ/サブスクライブ モデル (1) パブリッシュ/サブスクライブ モデルを 1 つの記事で理解する - Tencent Cloud Developer Community-Tencent Cloud (tencent.com) ↩︎

おすすめ

転載: blog.csdn.net/jmpei32534/article/details/128293992