项目实训--Unity多人游戏开发(七、pve游戏、工厂设计模式、墙壁)

文章目录

    • 本期简要说明
    • unity开发技巧
    • unity中的工厂模式
    • 递归切割生成随机“墙壁”
    • 雷电(协程提示)与下雨环境
    • 补间动画
    • Pun的RPC(远程过程调用)
    • 总结

本期简要说明

参考了b站的一个视频,不过我记得那个视频也是参考的别的平台…

主要介绍随机墙壁、打雷功能。
代码方面介绍工厂设计模式、协程。
此游戏中的伤害系统暂时就不多介绍了。注意一下多人化时要通过photonView.IsMine区分,让人物的真实玩家那边扣分就可以了。僵尸的扣血则是由主机负责,僵尸血量可以通过实现IPunObserver接口来同步(详情见官网的射击同步)。

unity开发技巧

  1. 协程中的延时操作。一个方法xxx写为IEnumerator类型,内部即可使用yield return语句来延迟执行。然后通过StartCoroutine(xxx)启动协程。常用的有yield return new WaitForSeconds(delayTime);yield return new WaitForEndOfFrame();后者一般用于需要每帧更新UI的情况。比如通过协程做一个倒计时,每帧减去时间后,把时间显示在屏幕上。
    Start函数也可以写为IEnumerator类型!
  2. Invoke的延时调用,也可以循环延时执行,但是不能在一个方法中实现多次且不同长度的延时。
  3. transform.DOScale等方法可以使“动画”更流畅。

unity中的工厂模式

Unity 通过CreateAssetMenu为使用自定义资源(.asset) 添加 Assets 菜单按钮,详细请看这里
项目实训--Unity多人游戏开发(七、pve游戏、工厂设计模式、墙壁)_第1张图片
项目实训--Unity多人游戏开发(七、pve游戏、工厂设计模式、墙壁)_第2张图片
项目实训--Unity多人游戏开发(七、pve游戏、工厂设计模式、墙壁)_第3张图片

[CreateAssetMenu(fileName = "MapFactory", menuName = "Factory/MapFactory")]
public class MapFactory : FactorySO<GameObject>
{
    //unity的工厂设计模式,需要获得floor和barrier的地方可以通过这个工厂.create...()获得

    public GameObject floorPrefab;
    public GameObject BarrierPrefab;

    public override GameObject Create()
    {
        GameObject floorInstance = Instantiate(floorPrefab);
        return floorInstance;
    }
    public GameObject CreateBarrier()
    {
        GameObject BarrierInstance = Instantiate(BarrierPrefab);
        return BarrierInstance;
    }
}

//应用时:
GameObject newFloor = MapFactory.Create();//创建一个floor实体

递归切割生成随机“墙壁”

可以通过墙壁的位置修改其材质颜色使之具有渐变色,可能更加美观。
墙壁之间高度不一增加了层次感。但是可能看起来比较乱。
墙壁移动用了dotween补间动画插件,动画更加流畅。

public void UpdateBarrier()
    {
        RandomMaze randomMaze = new RandomMaze((int)mapSize.y, (int)mapSize.x);//初始化一个迷宫对象
        randomMaze.BarrierMap(0, (int)mapSize.y , (int)mapSize.x , 0);//随机生成墙壁(迷宫)
        //迷宫暂时是以二维bool型数组--Maze记录的,false为墙壁,true为通路。生成迷宫后强制使中心点打通不允许有墙壁。
        randomMaze.Maze[(int)(mapSize.y / 2), (int)(mapSize.x / 2 )] = true;
        randomMaze.Maze[(int)(mapSize.y / 2 - 1), (int)(mapSize.x / 2) - 1] = true;
        randomMaze.Maze[(int)(mapSize.y / 2 - 1), (int)(mapSize.x / 2)] = true;
        randomMaze.Maze[(int)(mapSize.y / 2 - 1), (int)(mapSize.x / 2) + 1] = true;
        randomMaze.Maze[(int)(mapSize.y / 2), (int)(mapSize.x / 2) - 1] = true;
        randomMaze.Maze[(int)(mapSize.y / 2), (int)(mapSize.x / 2) + 1] = true;
        randomMaze.Maze[(int)(mapSize.y / 2) + 1, (int)(mapSize.x / 2) - 1] = true;
        randomMaze.Maze[(int)(mapSize.y / 2) + 1, (int)(mapSize.x / 2)] = true;
        randomMaze.Maze[(int)(mapSize.y / 2) + 1, (int)(mapSize.x / 2) + 1] = true;

        GameObject barrierPosition =  Instantiate(BarrierParent, this.transform);//创建墙壁父物体,位置和Map对象一样
        barrierPosition.transform.localPosition -= new Vector3(0, 1.5f, 0);//位置往下移1.5个单位
        currentBarriers = barrierPosition;//这俩都是指这个父物体了,前者是全局变量用以之后控制墙壁的消失,后者是私有变量无法在这个方法外调用。
        for (int i = 0; i < mapSize.y; i++)
        {
            for (int j = 0; j < mapSize.x; j++)
            {
                if(randomMaze.Maze[i,j]==false)//找到应该生成墙壁的地方
                {
                    GameObject newBarrier = MapFactory.CreateBarrier();//工厂创建墙壁
                    newBarrier.transform.SetParent(barrierPosition.transform);//设置墙壁的父物体是这个东西
                    //随机墙壁高度以参差不齐增加层次感,0~0.5,对应这个方法最后的0.5,刚好能使墙壁不会浮空(tips:物体transform设置的是几何中心)
                    float randomHeight = (float)UnityEngine.Random.Range(0, 5) / 10;
                    Vector3 NewPos = floorPosList[i, j].GetComponent<Floor>().FloorPos;//提取出这个地板对应的位置
                    newBarrier.transform.localPosition = NewPos;//设置上这个位置
                    newBarrier.transform.localScale = new Vector3(1 - outlinePercent, 1, 1 - outlinePercent);//x和z缩放0.9

                    //设置墙壁高度,可以追过去看代码,设置了相反高度--中心点在地面以下
                    //后面还有个整体上浮的方法,这样就理解为什么一开始把这些方块设的比较低(高度设为负)了
                    newBarrier.GetComponent<Barrier>().SetBarrier(randomHeight);
                    //SetBarrier里用了invoke,有时候巨好用!!!之前不知道然后选择其他方式写好久
                    #region colorSet
                    float colorPercentage;//这个region里写的是根据墙壁从左到右的距离,进行一个外观颜色渐变
                    colorPercentage =j/mapSize.x;
                    MeshRenderer meshRender = newBarrier.GetComponent<MeshRenderer>();
                    meshRender.material.color = Color.Lerp(initialColor, endColor, colorPercentage);
                    #endregion
                }
            }
        }
        barrierPosition.transform.DOMoveY(0.5f, 3f).SetEase(Ease.OutCubic);//y方向上的平移。应该是持续3秒的向上平移!到!0.5√√√
    }
    //方法作用:随机墙壁。
    //大致思路是切割此方形地图,十字刀切割。切出左上、右上、左下、右下四个板块,对这四个方形子板块递归切割直到板块过小无法切割。
    //解释:切割处生成墙壁。
    public void BarrierMap(int topIndex, int bottomIndex, int rightIndex, int leftIndex)
    {
        int rowIndex, cloumnIndex;
        int top = topIndex, bottom = bottomIndex, right = rightIndex, left = leftIndex;
        if (right - left < 2 || bottom - top < 2)         //当切割到只剩一列时就return---停止递归的条件。
        {
            return;
        }
        //只能在偶数行生成墙壁,为了确保有道路    [这行注释是作者写的←]
        //TODO 让地图更自然
        do
        {
            cloumnIndex = UnityEngine.Random.Range(left, rightIndex);//列
            rowIndex = UnityEngine.Random.Range(top, bottomIndex);//行
        }
        while ((rowIndex + 1) % 2 != 0 || (cloumnIndex + 1) % 2 != 0);//随机出两个奇数下标(地图第偶数行),然后以此坐标为中心的十字路线全为false

        for (int i = left; i < right; i++)
        {
            Maze[rowIndex, i] = false;
        }
        for (int j = top; j < bottom; j++)
        {
            Maze[j, cloumnIndex] = false;
        }
        //setDoor::“设置门、打通墙壁”,覆盖此中心点的随机大小上四个边界处打通两个墙壁
        //可以理解为:上面代码生成了一个十字形墙壁,我拿一个随机大小的桶盖住这个十字的中心点,然后碰到桶壁的那4个地方被压扁不再有墙壁--形成通路(即门)。
        SetDoor(top, bottom, right, left, rowIndex, cloumnIndex);

        //递归继续切割被这个十字切出来的四个小版块。
        BarrierMap(top, rowIndex, cloumnIndex, left);
        BarrierMap(rowIndex + 1, bottom, cloumnIndex, left);
        BarrierMap(top, rowIndex, rightIndex, cloumnIndex + 1);
        BarrierMap(rowIndex + 1, bottom, rightIndex, cloumnIndex + 1);
    }
    void SetDoor(int topIndex, int bottomIndex, int rightIndex, int leftIndex,int rowIndex,int columnIndex)
    {
        int top,bottom,right,left;
        do
        {
            top = UnityEngine.Random.Range(topIndex, rowIndex);
            bottom = UnityEngine.Random.Range(rowIndex, bottomIndex);
            left = UnityEngine.Random.Range(leftIndex, columnIndex );
            right = UnityEngine.Random.Range(columnIndex, rightIndex);
  
        }
        while (top % 2 != 0 || bottom % 2 != 0 || left % 2 != 0 || right % 2 != 0);
        //在当前规定的范围以及其中这个点随机出一个覆盖此点的小范围,且边界都是偶数

        //在这随机出来的小范围上:
        //这一行上从左右边界开始各向内打通两个点
        Maze[rowIndex,right] = true;
        Maze[rowIndex, right-1] = true;
        Maze[rowIndex, left] = true;
        Maze[rowIndex, left+1] = true;
        //这一列上从上下边界开始各向内打通两个点
        Maze[top, columnIndex] = true;
        Maze[top+1, columnIndex] = true;
        Maze[bottom-1, columnIndex] = true;
        Maze[bottom, columnIndex] = true; 
    }

雷电(协程提示)与下雨环境

可以跟随玩家也可以随机。
这是随机的情况,随机在一个通路的地方生成闪电,多端同步,所以在通知其他端生成闪电时随机就已经确定了。

    public void GenerateLightning()//闪电
    {
        int randomX = 0;
        int randomZ = 0;
        while (true)
        {
            randomX = Random.Range(0, 25);
            randomZ = Random.Range(0, 25);
            if(theMaze == null)
            {
                return;
            }
            else if(theMaze.Maze[randomX, randomZ])//是通路,生成闪电
            {
                break;
            }
        }
        //确定随机后再同步,而不是让每个客户端都去随机
        photonView.RPC("LightningRPC", RpcTarget.All, randomX, randomZ);
    }
    [PunRPC]
    private void LightningRPC(int x,int z)
    {
        StartCoroutine("Lightning", new object[] { x,z});
    }
    //协程,控制闪电、音效、地板提示等
    IEnumerator Lightning(object[] paras)
    {
        int x = (int) paras[0];
        int z = (int) paras[1];
        float spawnDelay = 2f, tileFlashSpeed = 4, timer = 0;
        
        Transform tile = GetTileByPlayerInt(x, z);
        if (tile != null)
        {
            //地板颜色闪烁提示
            Material tileMat = tile.GetComponent<MeshRenderer>().material;
            Color originColor = tileMat.color;
            Color flashColor = Color.red;
            while (timer < spawnDelay)
            {
                tileMat.color = Color.Lerp(originColor, flashColor, Mathf.PingPong(timer * tileFlashSpeed, 1));

                timer += Time.deltaTime;
                yield return null;
            }

            //音效及模型
            AudioManager.instance.Thunder(tile.position);
            GameObject lightning = Instantiate(punLightningPrefab, Vector3.zero, Quaternion.identity);
            GameObject collisionCube = Instantiate(punCollisionCubePrefab, tile.position, Quaternion.identity);
            lightning.GetComponent<LightningBoltScript>().StartPosition = new Vector3(10, 8, 5);
            lightning.GetComponent<LightningBoltScript>().EndPosition = tile.position;
            tileMat.color = originColor;
        }
    }

Color.Lerp是插值运算,也会使颜色变化更加流畅。
变化之后别忘了改回原先的颜色。
声明originColor(Color)的时候rgb参数是float的,与255的不匹配。所以单独记录一下原color,后再用其赋值即可。
最后部分是闪电预制体的生成,此游戏的伤害判断是闪电的地方会生成一个碰撞Cube,通过碰撞来检测是否闪电击中了玩家,毕竟闪电是个粒子特效,没有碰撞效果。

补间动画

DoTween插件,大家可以去DoTween官网学习。Unity官方资源中是免费的。

Pun的RPC(远程过程调用)

使用pun提供的rpc,以上闪电的生成等内容均需要rpc功能同步。
通过photonView.RPC(...)发起请求。
第一个参数是要执行那个方法的方法名,第二个参数是rpc对象。
别忘了在那个方法头部加上[PunRPC]以让他支持rpc调用。

总结

这个游戏是开发内容比较多的一个游戏。
包含生命值系统、僵尸的AI组件寻路、闪电(可以通过控制灯光和音效制造完美气氛)、枪械控制、墙壁的生成等。
部分代码采用单例模式和工厂设计模式。子弹可以采用对象池进行优化。

你可能感兴趣的:(unity,游戏,设计模式)