【Unity】RPG2D龙城纷争(二)关卡、地块

更新日期:2024年6月12日。

项目源码:后续章节发布

索引

简介

本章我们将从零开始实现关卡,不过,我们的目的是实现关卡与地块的基本逻辑,更复杂的功能我们打算在开发关卡编辑器时再做涉及,正所谓一步一个坎,就没有迈不过去的高山。

地块(Block)

地块作为关卡的组成元素,他是一个正方形的格子(当然也可以是菱形,针对斜视角游戏,不过这不在框架的支持范畴内,但做出一定的修改即可实现),每一个地块都有属于他自己的一些属性。

一、定义地块类

首先,我们定义地块类Block

csharp 复制代码
    /// <summary>
    /// 地块
    /// </summary>
    [DisallowMultipleComponent]
    public class Block : HTBehaviour
    {
        
    }

Block继承至HTBehaviour,使得它可以挂载到游戏物体上,然后作为一个地块对象(为不需要一个物体上多次挂载的组件定义DisallowMultipleComponent是一个好习惯,这将提高容错率,如果你的代码会给别人使用的话)。

很多同学热衷于在类继承链上规避MonoBehaviour,其实完全没有这个必要,只要你不滥用MonoBehaviour的生命周期,它将提供给你几大优势:

1.属性可编辑(Inspector面板);

2.属性可调试(Inspector面板);

3.对象可追踪(Scene里面有则有,无则无);

4.对象可销毁(当确定不再使用时,Destroy它,而不用置null后交给GC)。

二、地块类型

经过深思熟虑,我们将地块类型划分为如下几种:

  • 地面
  • 山体
  • 森林
  • 湖泊
  • 雪地
  • 障碍

编写代码:

csharp 复制代码
    /// <summary>
    /// 地块
    /// </summary>
    [DisallowMultipleComponent]
    public class Block : HTBehaviour
    {
        /// <summary>
        /// 类型
        /// </summary>
        [Label("类型")] public BlockType Type;
    }

/// <summary>
/// 地块类型
/// </summary>
public enum BlockType
{
    /// <summary>
    /// 地面
    /// </summary>
    [Remark("地面")]
    Ground = 0,
    /// <summary>
    /// 山体
    /// </summary>
    [Remark("山体")]
    Moutain = 1,
    /// <summary>
    /// 森林
    /// </summary>
    [Remark("森林")]
    Forest = 2,
    /// <summary>
    /// 湖泊
    /// </summary>
    [Remark("湖泊")]
    Water = 3,
    /// <summary>
    /// 雪地
    /// </summary>
    [Remark("雪地")]
    Snow = 4,
    /// <summary>
    /// 障碍
    /// </summary>
    [Remark("障碍")]
    Obstacle = 5
}

然后,我们设计每种类型的地块拥有的交互权限如下:

类型 角色可行走 角色可攻击(站在上面的敌人或穿过它攻击其他敌人)
地面
山体 受限(拥有飞檐走壁可行走,行走速度减1/2
森林 受限(行走速度减1/2
湖泊 受限(拥有踏水神行可行走,行走速度减1/2
雪地 受限(行走速度减2/3
障碍 受限(拥有隔山打牛可穿透障碍进行攻击

需注意的是,任意地块,当上面站有敌人时,将不可行走,不可跨越,当站有队友时,不可行走,可跨越。

也即是说,我们可以使用角色摆出特定的阵型,以拦住敌方的行走路线,或达到包围的效果。

三、地块渲染

地块的渲染我们决定选择SpriteRenderer,且一个地块仅渲染一张图片,那么,在Block类中需要持有地块渲染器的引用,我们添加代码:

csharp 复制代码
    /// <summary>
    /// 地块
    /// </summary>
    [DisallowMultipleComponent]
    public class Block : HTBehaviour
    {
        /// <summary>
        /// 目标渲染器
        /// </summary>
        [Label("目标渲染器")] public SpriteRenderer Target;
        /// <summary>
        /// 类型
        /// </summary>
        [Label("类型")] public BlockType Type;
    }

四、地块索引

然后,我们想一想地块还需要些什么属性,哦对了,我想我们在后续一定会需要检索地块(也即是根据索引找到一个地块),而我们的关卡采用二维平铺布局,所有地块存储的数据结构应当是一个二维数组最合适,所以地块的索引我们定义为二维的下标Vector2Int

csharp 复制代码
    /// <summary>
    /// 地块
    /// </summary>
    [DisallowMultipleComponent]
    public class Block : HTBehaviour
    {
        /// <summary>
        /// 目标渲染器
        /// </summary>
        [Label("目标渲染器")] public SpriteRenderer Target;
        /// <summary>
        /// 类型
        /// </summary>
        [Label("类型")] public BlockType Type;
        /// <summary>
        /// 位置
        /// </summary>
        [Label("位置")] public Vector2Int Pos;
    }

地块类的属性暂时就想到这么多,不必追求一次就考虑全面,后续根据情况补充即可,接下来我们定义关卡类。

关卡(Level)

关卡用于容纳并绘制一系列地块,以及后期容纳角色等其他的一系列东西。

一、定义关卡类

我们定义关卡类Level

csharp 复制代码
    /// <summary>
    /// 关卡
    /// </summary>
    [DisallowMultipleComponent]
    public class Level : SingletonBehaviourBase<Level>
    {
    }

Level继承至单例行为基类SingletonBehaviourBase,使得它可以挂载到游戏物体上,并作为单例始终全局唯一(同一时刻,场景中的关卡肯定只能有一个)。

二、关卡基础属性

我们先为关卡设计一些基础的属性:

csharp 复制代码
        /// <summary>
        /// 关卡索引
        /// </summary>
        [Label("关卡索引")] public int Index;
        /// <summary>
        /// 关卡名称
        /// </summary>
        [Label("关卡名称")] public string Name;
        /// <summary>
        /// 关卡背景音乐
        /// </summary>
        [Label("关卡背景音乐")] public AudioClip BGAudio;
        /// <summary>
        /// 地图
        /// </summary>
        [Label("地图")] public AStarGrid Map;
        /// <summary>
        /// 地图尺寸
        /// </summary>  
        [Label("地图尺寸")] public Vector2Int MapSize;
        /// <summary>
        /// 角色根节点
        /// </summary>
        [Label("角色根节点")] public Transform RolesRoot;
        /// <summary>
        /// 特效根节点
        /// </summary>
        [Label("特效根节点")] public Transform EffectsRoot;
属性名称 属性详解
关卡索引 关卡唯一标识符,也用作保存、加载关卡时的索引
地图 所有地块对象的根节点,此处类型为AStarGrid(A*寻路网格),兼并寻路功能
地图尺寸 地图的尺寸,用于描述地图的宽、高
角色根节点 所有角色对象的根节点
特效根节点 所有特效对象的根节点

地块、角色、特效都归于单一的根节点,即方便管理所有对象,又方便统一划层,也即是规定渲染器的遮挡层,这三者的遮挡层关系应当是:特效 > 角色 > 地块,具体如何实现遮挡,我们先不考虑这么多。

完事后我们的Level在层级面板应当是这样的(三个子对象也即是地块、角色、特效的根节点):

三、地块集合

定义一个二维数组存储所有地块:

csharp 复制代码
        /// <summary>
        /// 所有的地块
        /// </summary>
        public Block[,] Blocks { get; private set; }

此处注意,将其定义为property的原因是,使其规避序列化功能(因为不需要序列化,关卡在初始化时搜寻所有地块即可),且提升访问安全性。

四、关卡初始化

然后,定义一个初始化方法,用于在关卡加载到场景中后,执行他自身的所有初始化操作,此处我们避开MonoBehaviour的生命周期方法AwakeStart,因为在此处他们是不受控的,这也是我上面所提到的不滥用生命周期的另一个意思。

csharp 复制代码
        /// <summary>
        /// 初始化
        /// </summary>
        public virtual void Initialize()
        {
        	//Map为所有地块根节点,即可从Map搜寻所有地块
            Blocks = new Block[MapSize.x, MapSize.y];
            Block[] blocks = Map.GetComponentsInChildren<Block>(true);
            for (int i = 0; i < blocks.Length; i++)
            {
            	//地块的Pos下标,即代表了在二维数组中的索引,我们后续会开发关卡编辑器,Pos的赋值交由编辑器来完成,所以这里只管取Pos值
                Block block = blocks[i];
                Blocks[block.Pos.x, block.Pos.y] = block;
            }
        }

Initialize方法作为主动调用方法,在我们自行加载关卡完成后主动调用即可。

五、关卡销毁

同理,应当定义一个销毁方法,用于在关卡销毁时执行一些操作,虽然我们现在还没有需要做的(地块、角色、特效等都属于关卡物体子节点,会跟着一起销毁,无需额外操作),但事先将其规划好总没错。

csharp 复制代码
        /// <summary>
        /// 销毁
        /// </summary>
        public virtual void Dispose()
        {
            
        }

六、回合制逻辑

为了实现回合制逻辑,我们先定义如下几个属性:

csharp 复制代码
        /// <summary>
        /// 当前的回合
        /// </summary>
        [PropertyDisplay("当前的回合")]
        public int Round { get; private set; } = 1;
        /// <summary>
        /// 当前回合的行动阵营
        /// </summary>
        [PropertyDisplay("当前回合的行动阵营")]
        public RoleCamp RoundCamp { get; private set; } = RoleCamp.Player;
        /// <summary>
        /// 关卡状态
        /// </summary>
        [PropertyDisplay("关卡状态")]
        public LevelState State { get; private set; } = LevelState.InProgress;

    /// <summary>
    /// 角色阵营
    /// </summary>
    public enum RoleCamp
    {
        /// <summary>
        /// 玩家
        /// </summary>
        Player = 0,
        /// <summary>
        /// 敌人
        /// </summary>
        Enemy = 1
    }
    
    /// <summary>
    /// 关卡状态
    /// </summary>
    public enum LevelState
    {
        /// <summary>
        /// 进行中
        /// </summary>
        InProgress,
        /// <summary>
        /// 已通关
        /// </summary>
        Passed,
        /// <summary>
        /// 已失败
        /// </summary>
        Failed
    }

此处应该很好理解了,我们按字面意思来就行了,根据最初的设计,每一个回合:玩家先行动,然后是敌人行动,敌人行动完毕后此回合结束,进入下一回合(循环往复)

此时,我们发现,完整的回合制逻辑在未编写角色(Role)类前,并不太好写出来,所以我们先放下,将复杂的事情留到后面一步步拆解。

接下来我们准备引入角色(Role)类,不过,看了一眼窗外,今日天色已晚,不宜working...

那么,择日再战吧。

相关推荐
90后小陈老师10 小时前
Unity教学 项目2 2D闯关游戏
游戏·unity·游戏引擎
噗噗夹的TA之旅11 小时前
Unity Shader 学习20:URP LitForwardPass PBR 解析
学习·unity·游戏引擎·图形渲染·技术美术
nnsix11 小时前
Unity ReferenceFinder插件 多选资源查找bug解决
unity·游戏引擎·bug
gzroy12 小时前
Unity Shader Graph实现全息瞄准器
unity·游戏引擎
90后小陈老师15 小时前
Unity教学 基础介绍
unity·游戏引擎
90后小陈老师15 小时前
Unity教学 项目3 3D坦克大战
3d·unity·游戏引擎
秦奈17 小时前
Unity复习学习随笔(五):Unity基础
学习·unity·游戏引擎
nnsix18 小时前
Unity ReferenceFinder插件 窗口中选择资源时 同步选择Assets下的资源
java·unity·游戏引擎
麷飞花19 小时前
unity3d scene窗口选中物体, 在 hierarchy高光显示
unity·editor·unity3d·u3d·hierarchy
ۓ明哲ڪ20 小时前
Unity功能——关闭脚本自动编译(Unity2021.3)
unity·游戏引擎