【Unity】TankBattle联机坦克大战(三)创建关卡(生成地块)

更新日期:2026年8月12日。

项目源码:获取源码。

索引

创建关卡(生成地块)

在上一篇的关卡生成算法基础上,需要将最后生成的关卡数据(一个存储所有地块类型的二维数组)中所有的地块创建出来,才算完成了创建关卡的全部工作。

一、地块类

定义地块类TB_Block,一个该类实例对应关卡场景中一个地块(空白也算地块):

csharp 复制代码
    /// <summary>
    /// 地块
    /// </summary>
    public class TB_Block : HTBehaviour, IUpdateFrame
    {
    	//砖墙的渲染目标
        [SerializeField, ReadOnly] private GameObject _brickWall;
        //钢板的渲染目标
        [SerializeField, ReadOnly] private GameObject _steelPlate;
        //森林的渲染目标
        [SerializeField, ReadOnly] private GameObject _forest;
        //海水的渲染目标
        [SerializeField, ReadOnly] private GameObject _seawater;
        //雪地的渲染目标
        [SerializeField, ReadOnly] private GameObject _snowfield;
        //玩家老巢的渲染目标
        [SerializeField, ReadOnly] private GameObject _lair;
        //帧动画的间隔时长(存在动画的地块)
        [SerializeField] private float _animationInterval;

二、地块模板

除了空白地块以外,其他地块都需要渲染画面,最终选定使用Image组件来绘制地块,先创建一个TB_Block地块类的模板(后续用于克隆生成其他地块):

1.砖墙

砖墙最终的渲染图如下:

但由于其最小可拆分为16个组成片(4*4),所以其也即是由16个渲染目标组成:

因为其受到子弹攻击时,每次只会被削掉四分之一(玩家3星坦克发射的子弹每次可削掉二分之一)。

例如,砖墙左侧受击:

砖墙右侧受击:

砖墙上侧受击:

砖墙下侧受击:

所以砖墙会被拆分为16个组成片:

定义组成片类TB_Piece,用于描述地块拆分开的组成片

csharp 复制代码
    /// <summary>
    /// 地块的组成片
    /// </summary>
    public class TB_Piece : HTBehaviour
    {
        private RectTransform _rect;

        /// <summary>
        /// 位置组件
        /// </summary>
        public RectTransform Rect
        {
            get
            {
                if (_rect == null)
                {
                    _rect = transform.rectTransform();
                }
                return _rect;
            }
        }
        /// <summary>
        /// 是否已销毁
        /// </summary>
        public bool IsDestroy
        {
            get
            {
                return !gameObject.activeSelf;
            }
            set
            {
                gameObject.SetActive(!value);
            }
        }
    }

通过其IsDestroy属性可以设置、获取该片的销毁状态。

然后在地块类TB_Block初始化时搜寻并存储所有组成片:

csharp 复制代码
		//砖墙的组成片
        private TB_Piece[,] _brickWallPieces;

        protected override void Awake()
        {
            base.Awake();

            _brickWallPieces = new TB_Piece[4, 4];
            for (int i = 0; i < 4; i++)
            {
                for (int j = 0; j < 4; j++)
                {
                    TB_Piece piece = _brickWall.FindChildren($"Piece{i}_{j}").GetComponent<TB_Piece>();
                    _brickWallPieces[i, j] = piece;
                }
            }
        }

并提供方法可以获取指定的组成片:

csharp 复制代码
        /// <summary>
        /// 获取地块的组成片(仅针对砖墙或钢板)
        /// </summary>
        /// <param name="x">组成片的x坐标</param>
        /// <param name="y">组成片的y坐标</param>
        public TB_Piece GetPiece(int x, int y)
        {
            if (Type == BlockType.BrickWall)
            {
                if (x >= 0 && x < 4 && y >= 0 && y < 4)
                {
                    return _brickWallPieces[x, y];
                }
            }
            else if (Type == BlockType.SteelPlate)
            {
                if (x >= 0 && x < 2 && y >= 0 && y < 2)
                {
                    return _steelPlatePieces[x, y];
                }
            }
            return null;
        }

2.钢板

钢板最终的渲染图如下:

但由于其最小可拆分为4个组成片(2*2),所以其也即是由4个渲染目标组成:

因为其受到子弹攻击时,每次只会被削掉二分之一(仅限玩家3星坦克发射的子弹)。

所以钢板会被拆分为4个组成片:

然后在地块类TB_Block初始化时搜寻并存储所有组成片:

csharp 复制代码
		//钢板的组成片
        private TB_Piece[,] _steelPlatePieces;

        protected override void Awake()
        {
            base.Awake();

            _steelPlatePieces = new TB_Piece[2, 2];
            for (int i = 0; i < 2; i++)
            {
                for (int j = 0; j < 2; j++)
                {
                    TB_Piece piece = _steelPlate.FindChildren($"Piece{i}_{j}").GetComponent<TB_Piece>();
                    _steelPlatePieces[i, j] = piece;
                }
            }
        }

3.森林

森林最终的渲染图如下:

森林只需要展示一个静态图像即可,所以仅需一个渲染目标:

4.海水

海水最终的渲染图如下:

海水需要展示动态图像,使用两张渲染图轮番切换即可:

海水的动画效果并不影响游戏的帧同步进程,所以只需将其放到渲染帧里处理即可:

csharp 复制代码
		//每一个渲染帧回调一次
        public void OnUpdateFrame()
        {
            UpdateAnimation();
        }

        /// <summary>
        /// 更新地块动画
        /// </summary>
        private void UpdateAnimation()
        {
            switch (Type)
            {
                case BlockType.Seawater:
                    _seawaterTimer += Time.deltaTime;
                    if (_seawaterTimer >= _animationInterval)
                    {
                        _seawaterTimer -= _animationInterval;
                        //将海水第二帧画面逆向激活,从而产生动画效果
                        _seawaterPiece2.SetActiveInverse();
                    }
                    break;
            }
        }

动画很简单,但基本还原原版效果。

5.雪地

雪地最终的渲染图如下:

雪地只需要展示一个静态图像即可,所以仅需一个渲染目标:

6.玩家老巢

玩家老巢最终的渲染图如下:

玩家老巢虽然也是静态图像,但也需要使用两张渲染图,因为在被破坏后,会切换为破坏形态:

破坏形态:

三、更新地块图形

每一个地块创建出来后,可以通过Type属性为其设置类型:

csharp 复制代码
    /// <summary>
    /// 地块
    /// </summary>
    public class TB_Block : HTBehaviour, IUpdateFrame
    {
    	//图形脏状态
        private bool _isUpdateGraphic = false;
        private BlockType _type;

        /// <summary>
        /// 地块类型
        /// </summary>
        public BlockType Type
        {
            get
            {
                return _type;
            }
            set
            {
                _type = value;
                _isUpdateGraphic = true;
            }
        }
    }

每一次类型改变,都将触发图形脏状态,然后在渲染帧中会自动更新图形:

csharp 复制代码
        public void OnUpdateFrame()
        {
            if (_isUpdateGraphic)
            {
                _isUpdateGraphic = false;
                UpdateGraphic();
            }
        }

        /// <summary>
        /// 更新地块图形(根据地块类型,显示其对应的渲染目标)
        /// </summary>
        private void UpdateGraphic()
        {
            _brickWall.SetActive(false);
            _steelPlate.SetActive(false);
            _forest.SetActive(false);
            _seawater.SetActive(false);
            _snowfield.SetActive(false);
            _lair.SetActive(false);

            switch (Type)
            {
                case BlockType.BrickWall:
                    _brickWall.SetActive(true);
                    break;
                case BlockType.SteelPlate:
                    _steelPlate.SetActive(true);
                    break;
                case BlockType.Forest:
                    _forest.SetActive(true);
                    break;
                case BlockType.Seawater:
                    _seawater.SetActive(true);
                    break;
                case BlockType.Snowfield:
                    _snowfield.SetActive(true);
                    break;
                case BlockType.Lair:
                    _lair.SetActive(true);
                    break;
            }
        }

四、设置地块位置

每一个地块都有其索引位置,左下角地块为(0,0),右上角地块为(12,12),设置其索引位置,其便会自动放置到地图上的真实位置:

csharp 复制代码
        private Vector2Int _index;
        private Vector2Int _pos;

        /// <summary>
        /// 在地图上的索引
        /// </summary>
        public Vector2Int Index
        {
            get
            {
                return _index;
            }
            set
            {
                _index = value;
                //根据索引位置,计算真实位置
                Pos = new Vector2Int(_index.x * BlockSize + BlockSizeHalf, _index.y * BlockSize + BlockSizeHalf);
            }
        }
        /// <summary>
        /// 在地图上的真实位置
        /// </summary>
        public Vector2Int Pos
        {
            get
            {
                return _pos;
            }
            private set
            {
                _pos = value;
                Rect.anchoredPosition = _pos;
            }
        }

五、地图尺寸

为此,我们要先一步定义地图相关的尺寸参数,如下:

csharp 复制代码
        /// <summary>
        /// 地图的尺寸
        /// </summary>
        public static readonly int MapSize = 650;
        /// <summary>
        /// 地图行数
        /// </summary>
        public static readonly int Rows = 13;
        /// <summary>
        /// 地图列数
        /// </summary>
        public static readonly int Cols = 13;
        /// <summary>
        /// 地块、坦克尺寸
        /// </summary>
        public static readonly int BlockSize = 50;
        /// <summary>
        /// 地块、坦克尺寸的二分之一
        /// </summary>
        public static readonly int BlockSizeHalf = 25;
        /// <summary>
        /// 地块、坦克尺寸的四分之一(12.5),用于精准定位到砖墙的组成片
        /// </summary>
        public static readonly FixFloat BlockSizeQuarter = new FixFloat(12, 5);

六、生成地块方法

定义一个生成地块的通用方法:

csharp 复制代码
		//背景地块根节点(被坦克挡住)
        [InjectPath("Map/Background")] private Transform _rootBackground;
        //前景地块根节点(挡住坦克)
        [InjectPath("Map/Foreground")] private Transform _rootForeground;
		//地块模板
        [InjectPath("Map/BlockTemp")] private TB_Block _blockTemp;

        /// <summary>
        /// 生成一个地块
        /// </summary>
        private TB_Block GenerateBlock(int indexX, int indexY, BlockType blockType)
        {
        	//通过模板克隆地块
            GameObject obj = Main.CloneGameObject(_blockTemp.gameObject, true);
            obj.name = $"Block({indexX}-{indexY})";

            TB_Block block = obj.GetComponent<TB_Block>();
            block.Type = blockType;
            block.Index = new Vector2Int(indexX, indexY);

            //森林地块为前景,会挡住坦克
            if (blockType == BlockType.Forest)
            {
                obj.transform.SetParent(_rootForeground);
            }
            //其他地块为背景
            else
            {
                obj.transform.SetParent(_rootBackground);
            }

            return block;
        }

七、生成所有地块

万事具备,现在就可以根据关卡生成算法产生的数据来生成所有地块了:

csharp 复制代码
        /// <summary>
        /// 创建关卡地图中的所有地块
        /// </summary>
        private void CreateMap()
        {
        	//调用关卡生成算法,生成地图数据map
            BlockType[,] map = GetRegion<GenerateMapRegion>().GenerateMap();

            //创建地图中的所有地块
            _blocks = new TB_Block[Rows, Cols];
            for (int r = 0; r < Rows; r++)
            {
                for (int c = 0; c < Cols; c++)
                {
                    _blocks[r, c] = GenerateBlock(r, c, map[r, c]);
                }
            }

            //老巢的保护方块削掉一半
            _blocks[5, 0].GetPiece(0, 0).IsDestroy = true;
            _blocks[5, 0].GetPiece(0, 1).IsDestroy = true;
            _blocks[5, 0].GetPiece(0, 2).IsDestroy = true;
            _blocks[5, 0].GetPiece(0, 3).IsDestroy = true;
            _blocks[5, 0].GetPiece(1, 0).IsDestroy = true;
            _blocks[5, 0].GetPiece(1, 1).IsDestroy = true;
            _blocks[5, 0].GetPiece(1, 2).IsDestroy = true;
            _blocks[5, 0].GetPiece(1, 3).IsDestroy = true;

            _blocks[5, 1].GetPiece(0, 0).IsDestroy = true;
            _blocks[5, 1].GetPiece(0, 1).IsDestroy = true;
            _blocks[5, 1].GetPiece(0, 2).IsDestroy = true;
            _blocks[5, 1].GetPiece(0, 3).IsDestroy = true;
            _blocks[5, 1].GetPiece(1, 0).IsDestroy = true;
            _blocks[5, 1].GetPiece(1, 1).IsDestroy = true;
            _blocks[5, 1].GetPiece(1, 2).IsDestroy = true;
            _blocks[5, 1].GetPiece(1, 3).IsDestroy = true;
            _blocks[5, 1].GetPiece(2, 2).IsDestroy = true;
            _blocks[5, 1].GetPiece(2, 3).IsDestroy = true;
            _blocks[5, 1].GetPiece(3, 2).IsDestroy = true;
            _blocks[5, 1].GetPiece(3, 3).IsDestroy = true;

            _blocks[6, 1].GetPiece(0, 2).IsDestroy = true;
            _blocks[6, 1].GetPiece(0, 3).IsDestroy = true;
            _blocks[6, 1].GetPiece(1, 2).IsDestroy = true;
            _blocks[6, 1].GetPiece(1, 3).IsDestroy = true;
            _blocks[6, 1].GetPiece(2, 2).IsDestroy = true;
            _blocks[6, 1].GetPiece(2, 3).IsDestroy = true;
            _blocks[6, 1].GetPiece(3, 2).IsDestroy = true;
            _blocks[6, 1].GetPiece(3, 3).IsDestroy = true;

            _blocks[7, 0].GetPiece(2, 0).IsDestroy = true;
            _blocks[7, 0].GetPiece(2, 1).IsDestroy = true;
            _blocks[7, 0].GetPiece(2, 2).IsDestroy = true;
            _blocks[7, 0].GetPiece(2, 3).IsDestroy = true;
            _blocks[7, 0].GetPiece(3, 0).IsDestroy = true;
            _blocks[7, 0].GetPiece(3, 1).IsDestroy = true;
            _blocks[7, 0].GetPiece(3, 2).IsDestroy = true;
            _blocks[7, 0].GetPiece(3, 3).IsDestroy = true;

            _blocks[7, 1].GetPiece(2, 0).IsDestroy = true;
            _blocks[7, 1].GetPiece(2, 1).IsDestroy = true;
            _blocks[7, 1].GetPiece(2, 2).IsDestroy = true;
            _blocks[7, 1].GetPiece(2, 3).IsDestroy = true;
            _blocks[7, 1].GetPiece(3, 0).IsDestroy = true;
            _blocks[7, 1].GetPiece(3, 1).IsDestroy = true;
            _blocks[7, 1].GetPiece(3, 2).IsDestroy = true;
            _blocks[7, 1].GetPiece(3, 3).IsDestroy = true;
            _blocks[7, 1].GetPiece(0, 2).IsDestroy = true;
            _blocks[7, 1].GetPiece(0, 3).IsDestroy = true;
            _blocks[7, 1].GetPiece(1, 2).IsDestroy = true;
            _blocks[7, 1].GetPiece(1, 3).IsDestroy = true;
        }

针对玩家老巢四周的保护砖墙,将其指定的组成片削掉后,便完成了最终的关卡地图创建:

相关推荐
淡海水2 小时前
C#与常用数据结构源码剖析-全篇导览
开发语言·数据结构·unity·面试·职场和发展·c#
玖玥拾20 小时前
Unity3D RPG 入门项目(四)背包拖拽、物品悬浮提示、项目开发思维
3d·unity·游戏引擎
xcLeigh1 天前
Unity基础:Start与Update方法——Unity脚本生命周期初探
java·unity·游戏引擎·教程
郝学胜-神的一滴1 天前
[简化版 GAMES 104] 现代游戏引擎 05:游戏引擎世界构建核心机制深度解析
c++·程序人生·unity·游戏引擎·计算机图形学·opengl
松树戈2 天前
godot项目【宝石捕手】04~添加音效、记分
游戏引擎·godot
LONGZETECH2 天前
无人机实训高成本痛点解法:虚拟仿真实现 70% 耗材损耗下降
大数据·算法·unity·架构·无人机
五仁烧饼3 天前
Unity Addressables 静默资源策略
游戏·unity·addressables
ue星空3 天前
【Godot】更换编辑器外观主题
编辑器·游戏引擎·godot
fanfan_hongyun3 天前
unity 与cad 坐标系映射
unity·游戏引擎