unity 貪喫蛇作戰—2

  今天接着上個講,上次講到了玩家蛇和BaseSnake類了,下面講講AI蛇了,這裏我寫的AI是比較簡單了。AI蛇的AI邏輯是這樣的,蛇主要存在2中狀態,一種是漫遊狀態,漫遊狀態每隔多少秒,給蛇一個方向,然後蛇慢慢的旋轉到給的方向。這裏用的方法是前面講到的行爲那一節,簡單的畫個圖解釋一下

當AI蛇的方向爲A向量,這是我們需要要求蛇往B向量這個方向移動,這時的做法就是

B-A=C;那麼蛇的每幀的實際方向向量爲Dir=x1*A+C*x2;然後A=Dir向量,其中x1和x2是權重,可以隨便賦如果x1很小而x2很大的話 那麼蛇想B向量靠近的時間就會很短 相反就會很長。這個權重還有根據具體分析,如果AI蛇檢測到前面有一堵牆的話,這裏檢測沒有采用射線檢測因爲射線檢每幀檢測會比較消耗性能。所以採用的方法是在蛇頭前面添加2個碰撞器,判斷是左邊的碰撞器還是右邊的碰撞器碰撞到障礙(其他的蛇和邊界)來獲得躲避的向量。接下來貼出具體的AI代碼。

using UnityEngine;

namespace Snake
{
    public class AISnake : BaseSnake
    {

        [SerializeField] private TextMesh _textMesh;


        public AIRayCast LeftAiRayCast { set; get; }

        public AIRayCast RightAiRayCast { set; get; }

        public float DogRate { set; get; }

        private float _targetAngle;


        private float _wanderTime;
        private float _totalTime;
        private Vector3 steer;
        private bool b;
        private AISankesMrg _aiSankesMrg;

        public float Denominator { set; get; }

        public float Molecular { set; get; }


        public override void OnUpdate()
        {

            #region Hit

            if (LeftAiRayCast.IsHit && RightAiRayCast.IsHit)
            {
                b = true;
                _wanderTime = 0;
                _targetAngle = Random.Range(10, 30) + 90;
                steer = Quaternion.Euler(0, 0, _targetAngle) * MoveDir;

            }
            if (LeftAiRayCast.IsHit && !RightAiRayCast.IsHit)
            {
                b = true;
                _wanderTime = 0;
                _targetAngle = -Random.Range(10, 30) - 90;
                steer = Quaternion.Euler(0, 0, _targetAngle) * MoveDir;

            }
            if (!LeftAiRayCast.IsHit && RightAiRayCast.IsHit)
            {
                b = true;
                _wanderTime = 0;
                _targetAngle = Random.Range(10, 30) + 90;
                steer = Quaternion.Euler(0, 0, _targetAngle) * MoveDir;
            }
            
            #endregion

            #region NotHit

            if (!LeftAiRayCast.IsHit && !RightAiRayCast.IsHit)
            {
                _wanderTime += Time.deltaTime;
                if (_wanderTime >= _totalTime)
                {
                    b = true;
                    _wanderTime = 0;
                    _totalTime = Random.Range(1, 3);
                    steer = Random.insideUnitCircle.normalized;

                    Denominator = 20;
                    Molecular = 1;
                }
            }
            else
            {
                _wanderTime = 0;
            }

            Vector3 temp = steer - MoveDir;
            if (Vector2.Angle(steer, MoveDir) > 1)
            {
                if (b)
                {
                    Vector3 targetDir = (temp.normalized * Molecular + MoveDir * Denominator).normalized;
                    MoveDir = targetDir;
                }
            }
            else
            {
                b = false;
            } 

            #endregion

            LeftAiRayCast.IsHit = false;
            RightAiRayCast.IsHit = false;

            MoveSnake();
            RotateHead();

            Vector3 textPos = SnakeHead.transform.position + new Vector3(0, 0.3f, 0);
            _textMesh.transform.position = textPos;
        }

        public override void IntilizedSnakeNodes(int originCount, int skinIndex, string nickName = null)
        {
            _totalTime = Random.Range(1, 3);
            base.IntilizedSnakeNodes(originCount, skinIndex, nickName);
            SnakeHead.AddComponent<SnakeHeadRayCast>().IntilizedAiSanke(this);
            LeftAiRayCast = GenerateDetection("LeftDetection", new Vector2(-0.05f, 0.21f), new Vector2(0.08f, 0.15f));
            RightAiRayCast = GenerateDetection("RightDetection", new Vector2(0.05f, 0.21f), new Vector2(0.08f, 0.15f));

            Vector3 textPos = SnakeHead.transform.position + new Vector3(0, 0.3f, 0);
            _textMesh.transform.position = textPos;
            _textMesh.text = NickName;

        }

        public void IntilizedSnakeMrg(AISankesMrg mrg)
        {
            _aiSankesMrg = mrg;
        }

        public void Death()
        {
            _aiSankesMrg.RemoveSnake(this);
            for (int i = 1; i < SnakeNodes.Count; i++)
            {
                var node = SnakeNodes[i];
                node.transform.tag = "Food";
                node.layer = LayerMask.NameToLayer("Food");
                Vector2 pos = node.transform.position;
                pos = pos + Random.insideUnitCircle/10;
                if (i != 1)
                    node.transform.position = pos;
                Destroy(node.GetComponent<BodyIndex>());
                FoodsMrg.Instance.AddFood(SnakeNodes[i]);
            }
            Destroy(gameObject);
            _aiSankesMrg.AddToTenSnake();

        }


        private AIRayCast GenerateDetection(string name, Vector2 offset, Vector2 size)
        {
            var obj = new GameObject(name);
            var boxColider = obj.AddComponent<BoxCollider2D>();
            var aiRayCast = obj.AddComponent<AIRayCast>();
            aiRayCast.IntilizedAiSanke(this);
            obj.AddComponent<Rigidbody2D>().gravityScale = 0;
            boxColider.offset = offset;
            boxColider.size = size;
            boxColider.isTrigger = true;

            obj.transform.SetParent(SnakeHead.transform);
            obj.transform.localPosition = Vector3.zero;
            obj.transform.localEulerAngles = Vector3.zero;

            return aiRayCast;
        }
    }
}
首先重寫OnUpdate生命週期的方法。還有重寫了IntilizedSnakeNodes初始化的一個方法。然後寫了一個死亡Death方法,私有方法GenerateDetection作用是生成左右2個檢測器,最後一個方法就是初始化它的管理器AISankesMrg,AISankesMrg作用是管理所有的AI蛇。AISankesMrg主要作用是生成蛇,然後銷燬蛇,另外一個重要的作用的就是統一管理AI蛇的生命週期。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace Snake
{
    public class AISankesMrg
    {
        public static List<AISnake> AiSnakes;

        private List<Vector3> _pointList; 
        private GameObject _aiSnakePrefab;
        private int _posIndex;
        private int _snakeCount;
        public AISankesMrg(GameObject prefab, List<Vector3> pointList)
        {
            AiSnakes = new List<AISnake>();
            _pointList = pointList;
            _aiSnakePrefab = prefab;
            _snakeCount = 0;
        }

        public void OnUpdate()
        {
            for (int i = 0; i < AiSnakes.Count; i++)
            {
                AiSnakes[i].OnUpdate();
            }
        }

        public void AddToTenSnake()
        {
            int value = 10 - AiSnakes.Count;
            GenerateSnake(value);
        }

        public void GenerateSnake(int count)
        {
            List<int> ints = SkinsTable.Instance.SkinInts;
            for (int i = 0; i < count; i++)
            {
                int skinindex = ints[UnityEngine.Random.Range(0, ints.Count)];
                SkinsTable.Instance.GetItemByIndex(skinindex);
                AddSnake(i + 1 + _snakeCount, skinindex, _pointList[_posIndex]);

                _posIndex++;
                _posIndex %= _pointList.Count;
            }
            _snakeCount += count;
          
        }

        public void AddSnake(int snakeIndex,int skinIndex,Vector3 pos)
        {

            GameObject obj = GameObject.Instantiate(_aiSnakePrefab, new Vector3(0, 2, 0), Quaternion.identity);
            obj.name = "AiSnake" + snakeIndex;
            var aiSnake = obj.GetComponent<AISnake>();
            aiSnake.IntilizedSnakePos(pos, UnityEngine.Random.insideUnitCircle, snakeIndex);
            aiSnake.IntilizedSnakeNodes(10, skinIndex);
            aiSnake.IntilizedSnakeMrg(this);

            AiSnakes.Add(aiSnake);

        }

        public void ClearSnakes()
        {
            _snakeCount = 0;
            for (int i = 0; i < AiSnakes.Count; i++)
            {
                GameObject.Destroy(AiSnakes[i].gameObject);
            }
            AiSnakes.Clear();
        }

        public void RemoveSnake(AISnake aiSnake)
        {
            AiSnakes.Remove(aiSnake);
        }

    }
}
接下來貼出PlayerSnake的代碼,這段代碼就比較簡單了,首先重寫OnUpdate生命週期,一個初始化蛇

using com.klw.lib.core;
using MainGame;
using UnityEngine;

namespace Snake
{
    public class PlayerSnake:BaseSnake
    {

        [SerializeField] private TextMesh _textMesh;


        public override void IntilizedSnakeNodes(int originCount, int skinIndex, string nickName = null)
        {
            base.IntilizedSnakeNodes(originCount, skinIndex, nickName);
             SnakeHead.AddComponent<SnakeHeadRayCast>().IntilizedPlayerSanke(this);
             Vector3 textPos = SnakeHead.transform.position + new Vector3(0, 0.3f, 0);
             _textMesh.transform.position = textPos;
            _textMesh.text = NickName;

             MessagingSystem.Instance.AttachListener(typeof(SpeedUpMessage), SpeedUp);
        }

        public override void OnUpdate()
        {
            MoveSnake();
            RotateHead();

            Vector3 textPos = SnakeHead.transform.position + new Vector3(0, 0.3f, 0);
            _textMesh.transform.position = textPos;
        }

        public void SetMoveDir(Vector3 dirVector3)
        {
            var temp = new Vector2(dirVector3.x, dirVector3.z);
            if (temp != Vector2.zero)
                MoveDir = temp;
        }

        public void GameOver()
        {
            new GameOverMessage().Send();
            MessagingSystem.Instance.DetachListener(typeof (SpeedUpMessage), SpeedUp);
        }

        public bool SpeedUp(BaseMessage message)
        {
            if (message is SpeedUpMessage)
            {
                SpeeRatio = (message as SpeedUpMessage).SpeedRate;
            }
            return true;
        }

    }
}
IntilizedSnakeNodes方法被重寫了,重寫之後其實就添加了一個蛇的名字代碼和註冊了蛇加速消息。

SetMoveDir設置蛇移動的方向,這裏使用方法是  _joystackCc.DragAction = (v) => { _playerSnake.SetMoveDir(v); };給虛擬搖桿註冊DragAction,就是遙感產生了偏移後同時設置蛇的移動方向。

GameOver方法作用是通知遊戲結束。

接下來就是管理有戲整體的類GameLayer,這個類的作用管理UI管理器,聲音管理器(這裏我還沒有寫聲音),及玩家蛇 AI蛇管理器,這個類就是最高層的管理器了。

using System.Collections.Generic;
using com.klw.lib.core;
using DataBind.Core.Presentation;
using MainGame;
using Snake;
using UI;
using UnityEngine;

namespace Game
{
    public class GameLayer : MonoBehaviour
    {

        public enum GameState
        {
            None,
            GenerateFood,
            GeneratePlayerSnake,
            GameOver,
            Restart
        }

        private StateMachine<GameState> _fsm;
        private PlayerSnake _playerSnake;
        private AISankesMrg _aiSankesMrg;
      
        
       

        private bool _isPause;

        [SerializeField] private FoodsMrg _foodsMrg;
        [SerializeField] private int _foodPoolCount;
        [SerializeField] private GameObject _playerSnakePrefab;
        [SerializeField] private GameObject _aiSnakePrefab;
        [SerializeField] private CameraControl _cameraControl;
        [SerializeField] private List<Transform> _pointTransforms;
        [SerializeField] private JoystackCc _joystackCc;
        [SerializeField] private Canvas _canvas;

        public List<Vector3> PointList
        {
            get
            {
                List<Vector3> points = new List<Vector3>();
                for (int i = 0; i < _pointTransforms.Count; i++)
                {
                    points.Add(_pointTransforms[i].position);
                }

                return points;
            }
        }

        public static UIAdapter UiAdapter;
        public static PlayerDataPack PlayerDataPack;

        private void Awake()
        {
            _isPause = true;
            GetStr.LoadContent();
            PlayerDataPack = new PlayerDataPack();
            UiAdapter = new UIAdapter(PlayerDataPack);
           
            _fsm = StateMachine<GameState>.Initialize(this, GameState.None);
            _aiSankesMrg = new AISankesMrg(_aiSnakePrefab, PointList);
            UiAdapter.OpenPlayGameDialog();

            MessagingSystem.Instance.AttachListener(typeof(GameOverMessage), SetGameOverHandler);
            MessagingSystem.Instance.AttachListener(typeof(GamePlayMessage), SetGamePlayHandler);
            MessagingSystem.Instance.AttachListener(typeof(RestartGameMessage), RestartGameHandler);
           
        }

        private void Start()
        {
            _fsm.ChangeState(GameState.GenerateFood);
            _joystackCc.DragAction = (v) => { _playerSnake.SetMoveDir(v); };
            _canvas.GetComponent<ContextHolder>().Context = UiAdapter.MainGameMenuContext;

            InvokeRepeating("RankSnake", 0, 1);
        }

        private void Update()
        {
            if (!_isPause)
            {

                _playerSnake.OnUpdate();
                _cameraControl.OnUpdate();

                _aiSankesMrg.OnUpdate();
            }
        }

        private void RankSnake()
        {
            List<RankData> snakes = new List<RankData>();
            snakes.Add(new RankData(_playerSnake.NickName,_playerSnake.Count));
            for (int i = 0; i < AISankesMrg.AiSnakes.Count; i++)
            {
                var aisnake = AISankesMrg.AiSnakes[i];
                snakes.Add(new RankData(aisnake.NickName, aisnake.Count));
            }

            snakes.Sort();
            snakes.Reverse();
            if (UiAdapter.MainGameMenuContext.RankItems.Count == 0)
            {
                for (int i = 0; i < 5; i++)
                {
                    MainGameMenuContext.RankItem item = new MainGameMenuContext.RankItem();
                    item.NickName = snakes[i].NickName;
                    item.Length = snakes[i].Count;
                    item.RankNum = i + 1;

                    UiAdapter.MainGameMenuContext.RankItems.Add(item);
                }
            }
            else
            {
                for (int i = 0; i < UiAdapter.MainGameMenuContext.RankItems.Count; i++)
                {
                    MainGameMenuContext.RankItem item = UiAdapter.MainGameMenuContext.RankItems[i];
                    item.NickName = snakes[i].NickName;
                    item.Length = snakes[i].Count;
                    item.RankNum = i + 1;
                } 
            }
            
        }

        #region FSM

        [FSM("GenerateFood", FSMActionName.Enter)]
        private void GenerateFoodEnter()
        {
            for (int i = 0; i < _foodPoolCount; i++)
            {
                _foodsMrg.AddFood();
            }
            _fsm.ChangeState(GameState.GeneratePlayerSnake);
        }


        [FSM("GeneratePlayerSnake", FSMActionName.Enter)]
        private void GeneratePlayerSnakeEnter()
        {
            _playerSnake = Instantiate(_playerSnakePrefab).GetComponent<PlayerSnake>();
            _playerSnake.IntilizedSnakePos(new Vector3(0, 0, 0), new Vector3(1, 0, 0), 0);
            _playerSnake.IntilizedSnakeNodes(20, 2);
            PlayerDataPack.SnakeLength = _playerSnake.Count;
            _cameraControl.Intilized(_playerSnake.SnakeHead.transform);

            _aiSankesMrg.GenerateSnake(10);
           

        }


        [FSM("GameOver", FSMActionName.Enter)]
        private void GameOverEnter()
        {
            UiAdapter.OpenGameOverDialog();
        }


        [FSM("Restart", FSMActionName.Enter)]
        private void RestartGameEnter()
        {
            _foodsMrg.ClearFoods();
            _aiSankesMrg.ClearSnakes();
            Destroy(_playerSnake.gameObject);
            _playerSnake = null;

            PlayerDataPack.KillNum = 0;
            _fsm.ChangeState(GameState.GenerateFood);
            _isPause = false;
        }


        #endregion

        #region Message Handler

        private bool SetGamePlayHandler(BaseMessage message)
        {
            var msg = message as GamePlayMessage;
            if (msg != null)
            {
                _isPause = msg.IsPause;
            }
            return true;
        }

        private bool SetGameOverHandler(BaseMessage message)
        {
            var msg = message as GameOverMessage;
            if (msg != null)
            {
                _fsm.ChangeState(GameState.GameOver);
                _isPause = true;
              
            }
            return true;
        }

        private bool RestartGameHandler(BaseMessage message)
        {
            if (message is RestartGameMessage)
            {
                _fsm.ChangeState(GameState.Restart);
            }
            return true;
        } 

        #endregion

        
    }
}
當我們的UI和遊戲邏輯產生交互的時候,我們需要一個類似於消息處理機制的思想來交互,說白了這個消息處理機制(這個消息處理我之前寫過)就是單例,但是是封裝了一下的單例,讓我們理解起來比較容易同時爺比較容易管理。這個類的得功能是註冊所有的消息,及消息的處理,還有就是遊戲狀態的改變,然後就是管理整個遊戲的生命週期。
最後一節就把所有的代碼及剩下的思路講完,今天就講到這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章