项目实训--Unity多人游戏开发(十五、道具战)

文章目录

    • emm关于聊天功能的闲话
    • 道具战游戏简介
      • 截图
      • 游戏功能
    • 关键代码
      • 全局
      • 悬浮物道具
      • 实际道具
      • 人物脚本
    • 总结

emm关于聊天功能的闲话

聊天功能也是使用PhotonChat提供的API,比较方便的是参考demo,发消息的逻辑代码直接拿来用就好了。要注意目前pun版本的chat服务只有eu服务器。
然后本项目的聊天功能中UI是自主开发的。仿照英雄联盟的聊天样式,游戏内不会遮挡,并且一定时间没有消息后自动隐藏…

道具战游戏简介

部分资源是Unity官方商店(比如人物)、还有部分资源来自其他素材网站。

截图

生成角色。
项目实训--Unity多人游戏开发(十五、道具战)_第1张图片
随着时间场上生成了不少道具。
项目实训--Unity多人游戏开发(十五、道具战)_第2张图片
捡了一个隐身、导弹、地雷,还有瞬移突进技能。
项目实训--Unity多人游戏开发(十五、道具战)_第3张图片
按2发射导弹打坏木箱,附带粒子特效。
项目实训--Unity多人游戏开发(十五、道具战)_第4张图片

游戏功能

人物–控制系统、生命值系统、道具捡拾与使用系统。
整个场景–道具生成系统(道具悬浮效果)、草丛隐身机制。
道具–不同道具不同功能,其中有隐身道具要和草丛的隐身联动起来。
道具计划制作五个(导弹、地雷、恢复、隐身、疾步),额外制作一个冲刺位移技能。
其中隐身道具和进入草丛“隐身”的实现方式详见下一篇文章。


生成数有最大限制以控制游戏以适中的节奏进行,同时避免过多道具占用cpu资源。
但是currentCount的–有问题,不清楚原因。

关键代码

全局

PropsManager.cs:场景中道具生成器。

/// 
    /// 控制道具的生成
    /// 
    public class PropsManager : MonoBehaviour
    {
        public enum PropsType
        {
            None,
            MissileProp,  //炮弹
            LandmineProp, //地雷
            CloakProp,
            HealthBottleProp, //血瓶
            SpeedUpProp,//疾步
            FlashSkill
        }
        private string[] propsPrefabNameWithRandom
            = {"DroppedMissile", "DroppedMissile", "DroppedLandMine", "DroppedLandMine", "DroppedBottle", "DroppedRandomBox" };

        public static int mapSizeCanSpawn = 20;//default 20,用于随机范围
        public float spawnInterval = 4;//间隔:?秒一次
        private float currentTime = 4;
        public int spawnCountOneTime = 4;
        public int maxSpawnCountPermit = 20;//限制最大数量---弱限制,可19到19 + spawnCountOneTime。

        private static int currentCountInScene = 0;//当前场景中存在的数量

        private void Start()
        {
            if(!PhotonNetwork.IsMasterClient)
            {
                return;
            }
            maxSpawnCountPermit = PhotonNetwork.PlayerList.Length * 15;
        }

        private void Update()
        {
            //生成道具
            if(PhotonNetwork.IsMasterClient && PropsGameManager.instance.isGameStart)
            {
                //计时
                if(currentTime > 0)
                {
                    currentTime -= Time.deltaTime;
                }
                else
                {
                    //判断道具数是否到达上限
                    //if (currentCountInScene < maxSpawnCountPermit)
                    //{
                        for (int i = 0; i < spawnCountOneTime; i++)
                        {
                            /*
                            PhotonNetwork.Instantiate(
                                Const.PUN_PROPS_GAME_PREFABS_BASEPATH 
                                    + propsPrefabNameWithRandom[Random.Range(0, propsPrefabNameWithRandom.Length)]
                                , propsSpawnPoints[Random.Range(0, propsSpawnPoints.Length)].position
                                , Quaternion.identity);
                            */
                            Vector3 pos = new Vector3(Random.Range(-18f, 18f), 1, Random.Range(-18f, 18f));
                            PhotonNetwork.Instantiate(Const.PUN_PROPS_GAME_PREFABS_BASEPATH 
                                + propsPrefabNameWithRandom[Random.Range(0, propsPrefabNameWithRandom.Length)]
                                , pos
                                , Quaternion.identity);
                            currentCountInScene++;
                        }
                    //}
                    currentTime = spawnInterval;
                }
            }
        }

        public static void DecreaseCurrentCount()
        {
            currentCountInScene--;//TODO:线程是否安全
        }
    }

悬浮物道具

DroppedProps.cs:掉落物形式的道具(可捡状态,包含悬浮动作、保证生成在空旷处)

public class DroppedProps : MonoBehaviour//用dotween
    {
        public PropsManager.PropsType propType = PropsManager.PropsType.HealthBottleProp;
        private Vector3 startPos;
        void Start()
        {
            if(PhotonNetwork.IsMasterClient)
            {
                TestCollision();
            }
            Move();
        }

        //出生检测
        //静止物体没有刚体,通过物理检测来决定重新生成,同修改DroppedRandomProps脚本
        private void TestCollision()
        {
            float radius = 0.8f;
            Vector3 explosionPos = transform.position;
            Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);
            foreach (Collider hit in colliders)
            {
                if (hit.gameObject == this.gameObject)
                {
                    Debug.Log("self collision");
                    continue;
                }
                if (hit.gameObject.tag.Equals("Barrier") || hit.gameObject.tag.Equals("WoodenBox")
                    || hit.gameObject.tag.Equals("Grass") || hit.gameObject.tag.Equals("DroppedProps"))
                {
                    float sphereRadius = (float)PropsManager.mapSizeCanSpawn;
                    Vector3 pos = new Vector3(Random.Range(-sphereRadius, sphereRadius), 1, Random.Range(-sphereRadius, sphereRadius));
                    if(PhotonNetwork.IsMasterClient)
                    {
                        Debug.Log("发生碰撞self 刷新");
                        PhotonNetwork.Instantiate(Const.PUN_PROPS_GAME_PREFABS_BASEPATH + gameObject.name.Replace("(Clone)", ""), pos, Quaternion.identity);
                        PhotonNetwork.Destroy(gameObject);
                        return;
                    }
                }
            }
        }

        //悬浮
        private void Move()
        {
            startPos = transform.position;
            transform.DOPath(new Vector3[] { startPos, transform.position + new Vector3(0, 0.5f, 0), startPos }, 2)//Vector3.up//new Vector3(0,0.5f,0)
                .SetLoops(-1, LoopType.Restart);
            transform.DOBlendableLocalRotateBy(new Vector3(0, 360, 0), 4f, RotateMode.FastBeyond360)//.DORotate(new Vector3(0, 360, 45), 3f, RotateMode.WorldAxisAdd)
                .SetLoops(-1, LoopType.Restart);//循环设置为-1为一直
        }
      
        //销毁写在人物里面,背包满了道具碰到不销毁
    }

DroppedRandomProps.cs:因为隐身和疾步一些道具没有合适的3d资源。统一用一个“问号箱子”,也就是随机道具。
DroppedProps.cs基本一样,多了一个随机确定被捡的道具。


实际道具

PropMissile.cs:导弹道具(施放道具,非掉落物形式,为玩家使用后的真实导弹物体)
包含碰撞(爆炸)检测。和初始化问题。初始化时主要为调整导弹朝向。
(导弹模型默认导弹头是z轴正向的,所以要把z轴正向转向发射方向)

public class PropMissile : MonoBehaviour
    {
        public Player Owner { get; private set; }
        public GameObject explosionParticle;


        public void Start()
        {
            Destroy(gameObject, 3.0f);
        }
        public void OnTriggerEnter(Collider collision)
        {
            Debug.Log(collision.gameObject.name + "eee");//TODO:有时候会碰到自己?
            if (collision.gameObject.tag.Equals("DroppedProps") || collision.gameObject.tag.Equals("Grass"))
            {
                return;
            }
            if (collision.gameObject.tag.Equals("Player") && collision.gameObject.GetComponent<PhotonView>().Owner == Owner)
            {
                return;
            }
            float radius = 3.0f;
            Vector3 explosionPos = transform.position;
            Collider[] colliders = Physics.OverlapSphere(explosionPos, radius);

            bool clipIsPlayed = false;//如果破坏掉木箱,此bool控制只播放一次音效
            foreach (Collider hit in colliders)
            {
                if (hit.gameObject.tag.Equals("Player"))//打到玩家
                {
                    //TODO:不能打到自己
                    if(Owner.NickName.Equals(hit.gameObject.GetComponent<PhotonView>().Owner.NickName))
                    {
                        continue;
                    }
                    PropsPlayerController ppc = hit.gameObject.GetComponent<PropsPlayerController>();
                    PropsPlayerHealth pph = hit.gameObject.GetComponent<PropsPlayerHealth>();
                    PropsPlayerPropController pppc = hit.gameObject.GetComponent<PropsPlayerPropController>();
                    //玩家击飞
                    if (hit.attachedRigidbody)
                    {
                        hit.attachedRigidbody.AddExplosionForce(500, explosionPos, radius);
                    }
                    //掉血\动画\失控
                    if (ppc != null)
                    {
                        Debug.Log("调用switch方法");
                        pph.GetDamage(1, 1);
                        pppc.DoAnimationWithEffect(1);//动画与失控
                    }
                    //加分先不写
                }
                else if (hit.gameObject.tag.Equals("WoodenBox"))//打到木箱
                {
                    //音效,木箱摧毁
                    if(!clipIsPlayed)
                    {
                        clipIsPlayed = true;
                        AudioManager.instance.WoodBreak(transform.position);//只播放一次
                    }
                    Destroy(hit.gameObject);
                }
            }
            AudioManager.instance.MissileBomb(transform.position);
            //TODO:爆炸特效
            GameObject go = GameObject.Instantiate(explosionParticle, transform.position, Quaternion.identity);
            Destroy(go, 1);
            Destroy(gameObject);
        }


        public void Initialize(Player owner, Vector3 playerToMouse)//, Vector3 originalDirection , flaot lag
        {
            Owner = owner;
            /*
            Vector3 originalDirection = rotation * Vector3.forward;//平面,y=0,非标准化,xz是比例决定方向,数字绝对值最大为1
            transform.forward = originalDirection;
            */
            //transform.rotation = Quaternion.LookRotation(playerToMouse);
            //TODO:Quaternion.FromToRotation(vec1, vec2)//t.p, floor(mousePoint)
            Quaternion rotation = transform.rotation;
            Vector3 fromDir = rotation * Vector3.up;
            Vector3 axis = Vector3.Cross(fromDir, playerToMouse).normalized;
            float angle = Vector3.Angle(fromDir, playerToMouse);//俩向量角度
            transform.rotation = Quaternion.AngleAxis(angle, axis) * rotation;

            Rigidbody rigidbody = GetComponent<Rigidbody>();
            rigidbody.velocity = playerToMouse.normalized * 20;
            //rigidbody.position += rigidbody.velocity * lag;
        }
    }

其他道具就不介绍了。比如地雷的爆炸基本和导弹一样。血瓶恢复也好实现。


人物脚本

嗯比较复杂的是关于人物的脚本:
人物控制:涉及到Unity的一大利器:Ray,此应用为获取鼠标所指的方向,发射导弹时是由鼠标指向控制的。
PlayerController.cs
其中的SetInvisibleSetVisible方法是与隐身有关的方法(下一篇文章中的关闭渲染器方式)。

private void Update()
        {
            playerUICanvas.transform.rotation = Camera.main.transform.rotation;//TODO
        }
        private void FixedUpdate()//获取玩家对于枪械的输入-鼠标左键或R键、人物移动
        {
            //playerUICanvas.transform.rotation = Camera.main.transform.rotation;//TODO:分离
            if (!photonView.IsMine)//非自己退出
            {
                return;
            }
            
            if (transform.position.y < 0)
            {
                transform.position = new Vector3(0, 2, 0);
            }
            /*
            if (!canControll)
            {
                return;
            }
            */
            if(!canControll)
            {
                return;
            }
            Move();
            Turning();
        }
        #endregion

        #region public人物方法--  受伤、生命、移动、显示、动画
        
        public void Move()
        {
            Vector3 moveInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
            Vector3 moveVelocity = moveInput.normalized * moveSpeed;
            thisRg.MovePosition(thisRg.position + moveVelocity * Time.fixedDeltaTime);
            if(moveVelocity.magnitude > 0) animator.SetFloat("speed", 5);
            else animator.SetFloat("speed", -1);
        }
        public void Turning()
        {
            Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit floorHit;
            if (Physics.Raycast(camRay, out floorHit, 30f, floorMask))
            {
                Vector3 playerToMouse = floorHit.point - this.transform.position;
                playerToMouse.y = 0f;
                //y不是0,朝向看向地板
                Quaternion newRotatation = Quaternion.LookRotation(playerToMouse);
                //this.transform.DORotateQuaternion(newRotatation, 0.5f);//TODO:用刚体解决画布问题
                thisRg.rotation = newRotatation;//.MoveRotation(newRotatation);
            }
        }



        public void SetInvisible()
        {
            //把孩子的render关了
            transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().enabled = false;
            transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().enabled = false;
            playerUICanvas.gameObject.SetActive(false);
        }

        public void SetVisible()
        {
            //把孩子的render打开
            transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().enabled = true;
            transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().enabled = true;
            playerUICanvas.gameObject.SetActive(true);
        }

        //人物死后取消碰撞//TODO:生命值剩一个检测
        public void CloseCollider()
        {
            GetComponent<BoxCollider>().enabled = false;
        }

        #endregion

PlayerHealth.cs
生命值系统,此游戏生命值为UI直观显示。如截图那里人物头顶上包含昵称显示与生命值显示。由五个独立的红色小图形作为血条(五格血)。
这是包含了pun同步的脚本

public class PropsPlayerHealth : MonoBehaviour
    {
        [Header("==Health==")]
        int maxHealth = 5;
        int currentHealth;//=max

        [Header("==UI==")]
        public GameObject healthPanel; //血条面板
        public Text infoText;

        [Header("==Other==")]
        public PropsPlayerController ppc;

        private PhotonView photonView;


        private void Start()
        {
            currentHealth = maxHealth;
            photonView = this.GetComponent<PhotonView>();
            infoText = GameObject.Find("UI").transform.GetChild(0).GetComponent<Text>();
        }

        /// 
        /// damageSource = 1:导弹  2:地雷
        /// 
        /// 
        /// 
        public void GetDamage(int damageValue, int damageSource)//TODO:此游戏damageValue用不到,伤害设计为1
        {
            if (!photonView.IsMine)
            {
                return;
            }
            if (currentHealth <= 0)
            {
                return;
            }
            //TODO:如果排除他人执行代码则需要rpc执行血条更新和音效等
            currentHealth--;
            photonView.RPC("DecreaseHealth", RpcTarget.All, currentHealth);
            if (currentHealth <= 0)//死亡
            {
                ppc.canControll = false;

                //RPC
                photonView.RPC("PlayerDie", RpcTarget.All, PhotonNetwork.LocalPlayer.NickName, damageSource);

                //摄像头位置
                Camera.main.gameObject.GetComponent<AutoFollow>().enabled = true;

                //玩家死亡
                PhotonNetwork.LocalPlayer.SetCustomProperties(new ExitGames.Client.Photon.Hashtable() { { Const.PLAYER_LIVES, 0 } });
                //仅取消渲染而不销毁,留着脚本执行rpc等
                ppc.SetInvisible();//TODO:是否留有碰撞
                //PhotonNetwork.Destroy(gameObject);
            }
        }

        public void healthAdd()
        {
            if (currentHealth < maxHealth)
            {
                //通知所有人执行加血的UI
                photonView.RPC("AddHealth", RpcTarget.All, currentHealth);//加血
                currentHealth++;
            }
        }

        #region rpc
        [PunRPC]
        public void AddHealth(int currentHealth)//*******TODO:currentHealth++不需要出现在这里?//涉及到恢复
        {
            if (currentHealth >= 5)
            {
                return;
            }
            healthPanel.transform.GetChild(currentHealth).GetComponent<Image>().color = Color.red;
            //currentHealth++;
        }

        [PunRPC]
        public void DecreaseHealth(int currentHealth) //减少血量
        {
            AudioManager.instance.PropsGameGetDamage(transform.position);
            healthPanel.transform.GetChild(currentHealth).GetComponent<Image>().color = Color.gray;//TODO
        }

        [PunRPC]
        public void PlayerDie(string nickname, int damageSource)
        {
            AudioManager.instance.PlayerDie(transform.position);
            if (damageSource == 1)
                infoText.text = "玩家" + nickname + "勇敢的用肉身挡住了导弹!";
            else
                infoText.text = "玩家" + nickname + "用生命帮你排除了一颗地雷!";
            Invoke("EmptyInfoText", 3);
            //关闭渲染器和碰撞体等
            //ppc.SetInvisible();
            ppc.CloseCollider();
            ppc.canControll = false;
            GetComponent<Animator>().SetBool("die", true);
            Invoke("StopAnimationOnDieAnim", 0.5f);
        }
        #endregion

        private void StopAnimationOnDieAnim()
        { 
            //TODO:停止死亡动画保持死亡动作
        }

        private void EmptyInfoText()
        {
            infoText.text = "";
        }
    }

PlayerPropsController.cs
道具控制系统,“背包”、捡拾、道具使用。
其中的UseProp方法是使用各种道具的方法、OnTriggerEnter回调控制捡拾。
此代码默认3格背包满后再捡道具时顶替原来的第三格道具。
其中Cloak方法是道具技能的隐身(下一篇文章的摄像机layer遮罩方式)。

public class PropsPlayerPropController : MonoBehaviour
    {
        enum RenderingMode
        {
            Opaque,
            Transparent,
        }

        [Header("==游戏==")]
        public LayerMask floorMask;
        public PropsPlayerController ppc;
        private Rigidbody thisRg;
        private Animator animator;
        public Text tipText;
        


        [Header("==预制体==")]
        public GameObject missilePrefab;
		public GameObject landminePrefab; //放置的地雷预制体
		
        [Header("==背包==")]
        //第四位是闪现技能为1时表示冷却完毕,0时不可用
        public PropsManager.PropsType[] itemBag = new PropsManager.PropsType[4];
        public Transform bagItemsPanel;//背包的UI
        public Sprite missileSprite;
        public Sprite landMineSprite;
        public Sprite cloakSprite;
        public Sprite speedUpShoesSprite;
        public Sprite noneSprite;

        //[Header("==技能==")]
        //冲刺技能
        public float cdtime = 0;
        public float cd = 7;

        //加速的时间
        private static float staySpeedUpTime = 2f;
        private float currentSpeedUpTime = 0;
        private bool isSpeedUp = false;
        private float OldSpeed;//获取人物的初始速度

        PhotonView photonView;
        int noCameraLayer = 16;


        #region unity
        private void Start()
        {
            animator = this.GetComponent<Animator>();
            photonView = GetComponent<PhotonView>();
            thisRg = GetComponent<Rigidbody>();
            if (!photonView.IsMine)
            {
                return;
            }
            for (int i = 0; i < itemBag.Length; i++)
            {
                itemBag[i] = PropsManager.PropsType.None;
            }
            GameObject canvas = GameObject.Find("UI");
            bagItemsPanel = canvas.transform.GetChild(6).GetChild(1);
            tipText = canvas.transform.GetChild(1).GetComponent<Text>();
            OldSpeed = transform.gameObject.GetComponent<PropsPlayerController>().moveSpeed;
        }

        private void Update()
        {
            if(!photonView.IsMine)
            {
                return;
            }
            if(cdtime > 0)
            {
                cdtime -= Time.deltaTime;
                //TODO:UI
            }
            if(!ppc.canControll)
            {
                return;
            }

            if (isSpeedUp) //判断是否加速中
            {
                if(currentSpeedUpTime < staySpeedUpTime)
                {
                    currentSpeedUpTime += Time.deltaTime;
                }
                else  //加速时间到了
                {
                    isSpeedUp = false;
                    currentSpeedUpTime = 0;
                    UpandDownSpeed(OldSpeed);
                }
            }

            Operate();
        }

        public void OnTriggerEnter(Collider other)//TODO:同步背包
        {
            //IsPickingPropUp
            if (!other.gameObject.tag.Equals("DroppedProps"))
            {
                return;
            }
            if(other.gameObject.GetComponent<DroppedProps>() != null)
            {
                if (other.gameObject.GetComponent<DroppedProps>().propType == PropsManager.PropsType.HealthBottleProp)//血瓶不参与背包计算,且血瓶一个单独脚本执行逻辑
                {
                    return;
                }
            }

            //是否销毁(是否捡的起来)由玩家拥有者的背包判决。
            int index = 0;
            //***:主要用以通知主机,因为只有主机控制着销毁悬浮道具,可换为PhotonNetwork.IsMasterClient
            if (!photonView.IsMine)
            {
                if (PhotonNetwork.IsMasterClient)
                {
                    PhotonNetwork.Destroy(other.gameObject);
                    PropsManager.DecreaseCurrentCount();//场上数量-1
                }
                else
                {
                    other.gameObject.SetActive(false);
                }
            }
            else//自己
            {
                index = FindVacantIndex();
                DroppedProps dp = other.gameObject.GetComponent<DroppedProps>();
                DroppedRandomProps drp = other.gameObject.GetComponent<DroppedRandomProps>();
                if (dp != null)
                {
                    itemBag[index] = dp.propType; //添加背包里捡的道具的类型
                    //UI
                    bagItemsPanel.GetChild(index).GetComponent<Image>().sprite
                        = GetSpriteByPropType(dp.propType);
                }
                else if(drp != null)
                {
                    itemBag[index] = drp.propType; //添加背包里捡的道具的类型
                    //UI
                    bagItemsPanel.GetChild(index).GetComponent<Image>().sprite
                        = GetSpriteByPropType(drp.propType);
                }
                other.gameObject.SetActive(false);
            }
            
            AudioManager.instance.PropsGamePickUp();
        }
        #endregion

        #region key methods
        public void Operate()
        {
            if (Input.GetKeyDown(KeyCode.Alpha1))
            {
                //UseProp(PropsManager.PropsType.MissileProp);
                //根据itemBag[0]的值执行
                UseProp(itemBag[0]);
                itemBag[0] = PropsManager.PropsType.None;//背包栏位置空
                bagItemsPanel.GetChild(0).GetComponent<Image>().sprite = GetSpriteByPropType(PropsManager.PropsType.None);
            }
            if (Input.GetKeyDown(KeyCode.Alpha2))
            {
                //UseProp(PropsManager.PropsType.CloakProp);
                //UseProp(PropsManager.PropsType.SpeedUpProp);
                UseProp(itemBag[1]);
                itemBag[1] = PropsManager.PropsType.None;
                bagItemsPanel.GetChild(1).GetComponent<Image>().sprite = GetSpriteByPropType(PropsManager.PropsType.None);
            }
            if (Input.GetKeyDown(KeyCode.Alpha3))
            {
                //UseProp(PropsManager.PropsType.LandmineProp);
                UseProp(itemBag[2]);
                itemBag[2] = PropsManager.PropsType.None;
                bagItemsPanel.GetChild(2).GetComponent<Image>().sprite = GetSpriteByPropType(PropsManager.PropsType.None);
            }
            if (Input.GetKeyDown(KeyCode.E))
            {
                UseProp(PropsManager.PropsType.FlashSkill);
                //itemBag[3] = 0;
            }
        }

        private void UseProp(PropsManager.PropsType type)//TODO:E技能计时UI
        {
            switch (type)
            {
                case PropsManager.PropsType.None:
                    AudioManager.instance.NoProps();
                    break;
                case PropsManager.PropsType.MissileProp:
                    //不能直接用这个刚体的位置和转向,刚体位置要高一点从臂膀发出,转向要指向地面

                    //鼠标
                    Vector3 playerToMouse = transform.forward;
                    Ray camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
                    RaycastHit floorHit;
                    if (Physics.Raycast(camRay, out floorHit, 30f, floorMask))
                    {
                        //0.4比较精准触碰地板(考虑到导弹有体积可能会提前碰到地板)
                        playerToMouse = (floorHit.point + new Vector3(0, 0.4f, 0)) - (this.transform.position + Vector3.up);
                    }

                    //目前人物看向地板,应该可以直接用刚体朝向
                    photonView.RPC("Missile", RpcTarget.AllViaServer, thisRg.position + Vector3.up, playerToMouse);
                    break;
                case PropsManager.PropsType.LandmineProp:
                    //先判断当前执行的动画是不是站立idel
                    if (animator.GetCurrentAnimatorStateInfo(0).IsName("IdleNormal02_AR_Anim"))//TODO: para:layerIndex:0???
                    {
                        Debug.Log("执行放置地雷的操作1");
                        DoAnimationWithEffect(2);
                        //创建地雷的方法
                        StartCoroutine(CreateLandMine(this.gameObject.transform.position));
                    }
                    else//不在空闲状态无法放置
                    {
                        tipText.text = "放置被打断";
                        Invoke("EmptyTipText", 1.5f);
                    }
                    break;
                case PropsManager.PropsType.CloakProp:
                    photonView.RPC("Cloak", RpcTarget.AllViaServer);
                    break;

                case PropsManager.PropsType.FlashSkill:
                    if(cdtime > 0)
                    {
                        AudioManager.instance.NoProps();
                        return;
                    }
                    //瞬移
                    AudioManager.instance.PropsGameFlash(transform.position);
                    Ray mouseRay = Camera.main.ScreenPointToRay(Input.mousePosition);
                    RaycastHit hit;
                    Vector3 direction = Vector3.zero;
                    if (Physics.Raycast(mouseRay, out hit, 30f, floorMask))
                    {
                        direction = hit.point - this.transform.position;
                        direction.y = 0f;
                    }
                    transform.DOBlendableMoveBy(direction.normalized * 4, 0.5f);
                    cdtime = cd;
                    break;

                case PropsManager.PropsType.SpeedUpProp:
                    tipText.text = "速度提升";
                    Invoke("EmptyTipText", 1);
                    UpandDownSpeed(OldSpeed * 1.5f);
                    AudioManager.instance.PropGameSpeedUp(transform.position);
                    isSpeedUp = true;
                    break;
                default:
                    break;
            }
        }


        /// 
        /// type: 1击飞,2前倾放地雷。(起身动画由Animator自动跳转)
        /// 
        /// 
        public void DoAnimationWithEffect(int animationType)
        {
            if (!photonView.IsMine)
                return;
            switch (animationType)
            {
                case 1:
                    ppc.canControll = false;
                    animator.SetBool("fly", true);//被击飞,修改了string
                    Invoke("flyReset", 0.1f);//TODO:延时?其他方法?

                    Invoke("RestoreControl", 3);//TODO:要搭配击飞时间 + 起身时间
                    break;
                case 2://地雷,失控2秒,后续可以实现取消放置
                    if (!ppc.canControll)
                    {
                        return;
                    }
                    ppc.canControll = false;
                    animator.SetBool("setLandMine", true);
                    Invoke("RestoreControl", 3);//两秒后恢复移动,放置地雷就两秒钟
                    Invoke("setLandMineFalse", 0.1f);
                    break;
            }
        }


        private void flyReset()
        {
            animator.SetBool("fly", false);
        }

        private void RestoreControl()
        {
            ppc.canControll = true;
        }
        private void setLandMineFalse()
        {
            animator.SetBool("setLandMine", false);
        }

        #endregion

        #region rpc 使用道具
        [PunRPC]
        public void Missile(Vector3 position, Vector3 playerToMouse, PhotonMessageInfo info)//传方向
        {
            //可以通过info中的延迟差,精确同步导弹
            GameObject missile = Instantiate(missilePrefab, position, Quaternion.identity) as GameObject;
            missile.GetComponent<PropMissile>().Initialize(photonView.Owner, playerToMouse);
            AudioManager.instance.StartOffMissile(transform.position);
        }

        [PunRPC]
        public void LandMineCreate(Vector3 pos)
        {
            GameObject landmine = Instantiate(landminePrefab, pos, Quaternion.identity);
        }

        [PunRPC]
        public void Cloak()//隐身//TODO:累计隐身,需要改恢复状态逻辑
        {
            AudioManager.instance.Cloak(transform.position);
            //他端隐藏3秒
            //自己半透明3秒
            if (photonView.IsMine)
            {
                SetMaterialRenderingMode(transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Transparent);
                SetMaterialRenderingMode(transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Transparent);
                Invoke("RestoreVisibleFromCloakWithMaterial", 3f);
            }
            else
            {
                //考虑到在隐身途中进入到了草里结束隐身时,所以与草不同要用不同的“隐身”逻辑以规避和草丛隐身的bug
                transform.GetChild(0).gameObject.layer = noCameraLayer;
                transform.GetChild(2).gameObject.layer = noCameraLayer;
                transform.GetChild(5).gameObject.layer = noCameraLayer;
                Invoke("RestoreLayer", 3f);
            }
        }
        #endregion


        #region private methods
        //遍历背包返回满足的下标
        /// 
        ///  背包满时返回2,默认顶替最后一格。可考虑满返回-1,但是用不到这个逻辑。
        /// 
        private int FindVacantIndex()
        {
            for (int i = 0; i < itemBag.Length - 1; i++)//最后一格是技能不参与背包运算
            {
                if (itemBag[i] == PropsManager.PropsType.None)//i为空
                {
                    return i;
                }
            }
            return 2;
        }


        private void RestoreLayer()
        {
            transform.GetChild(0).gameObject.layer = 0;
            transform.GetChild(2).gameObject.layer = 0;
            transform.GetChild(5).gameObject.layer = 0;
        }


        private void RestoreVisibleFromCloakWithMaterial()
        {
            SetMaterialRenderingMode(transform.GetChild(0).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Opaque);
            SetMaterialRenderingMode(transform.GetChild(2).GetComponent<SkinnedMeshRenderer>().material, RenderingMode.Opaque);
        }

        private void SetMaterialRenderingMode(Material material, RenderingMode renderingMode)
        {
            switch (renderingMode)
            {
                case RenderingMode.Opaque:
                    material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
                    material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
                    material.SetInt("_ZWrite", 1);
                    material.DisableKeyword("_ALPHATEST_ON");
                    material.DisableKeyword("_ALPHABLEND_ON");
                    material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
                    material.renderQueue = -1;
                    break;

                case RenderingMode.Transparent:
                    material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
                    material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
                    material.SetInt("_ZWrite", 0);
                    material.DisableKeyword("_ALPHATEST_ON");
                    material.DisableKeyword("_ALPHABLEND_ON");
                    material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
                    material.renderQueue = 3000;
                    break;
            }
        }

        private void EmptyTipText()
        {
            tipText.text = "";
        }

        private void UpandDownSpeed(float s) //控制移动速度
        {
            transform.gameObject.GetComponent<PropsPlayerController>().moveSpeed = s;
        }
        //生成地雷
        private IEnumerator CreateLandMine(Vector3 v) //主机生成,不知道咋弄。。。。
        {
            AudioManager.instance.SetLandmine(v);
            yield return new WaitForSeconds(3); //等待3秒
            v.y = 0.001f;
            photonView.RPC("LandMineCreate", RpcTarget.AllViaServer, v);
       
        }

        private Sprite GetSpriteByPropType(PropsManager.PropsType propType)
        {
            switch (propType)
            {
                case PropsManager.PropsType.MissileProp:
                    return missileSprite;
                case PropsManager.PropsType.LandmineProp:
                    return landMineSprite;
                case PropsManager.PropsType.CloakProp:
                    return cloakSprite;
                case PropsManager.PropsType.SpeedUpProp:
                    return speedUpShoesSprite;
                case PropsManager.PropsType.None:
                    return noneSprite;
                default:
                    return missileSprite;
            }

        }
        #endregion
    }

总结

关键代码就这么多。
因为这是项目最后开发的一个游戏,当时对pun多人化比较熟悉了。所以两个人用了四天左右的时间就开发出来了,而且本来量就不怎么大。
一共5+1个道具技能。和全局道具生成器。还有比较复杂的人物脚本。
配上音效后这个游戏趣味性还是挺高的。

《关于此游戏》
忽略美观程度,这种玩法本身具有很高的趣味性。
毕竟由于不同道具的组合使用,再结合地形特点,很容易被玩家研究出专属的“组合技”。
比如使用隐身、加速道具,再加上一个突进技能,到你脸前放一枚导弹…
或者开着隐身跑到另一个草丛藏起来…额太搞了…

你可能感兴趣的:(unity,c#,游戏引擎)