unity3d做塔防游戏
今天我们将学习如何使用 Unity3D 制作塔防游戏。我发现了一篇非常好的相关文章,在此分享给大家,希望能在大家学习过程中有所帮助。
塔防游戏概述
通常来说,塔防游戏是一种策略类游戏,玩家需要在地图上建造炮台或类似建筑物来阻止敌人进攻。从这个概念中,我们可以提取出三个关键元素:地图(场景)、敌人和炮台(防守单位)。这样一来,塔防游戏可以描述为:敌人按照地图中预设的路径发起进攻,玩家利用防守单位进行防御的策略游戏。
经典的塔防游戏有很多,比如大家熟知的《植物大战僵尸》《保卫萝卜》。如果将塔防游戏中防守单位的范围扩大到玩家,像《英雄联盟》这类游戏也可以归类为塔防游戏。在《英雄联盟》中,敌我双方的最终目标都是摧毁敌方防御塔,只不过敌我角色从炮台或怪物变成了有血有肉的人物,并且融入了角色扮演(RPG)和即时战略(RTS)等元素,使其可玩性大大提高。
接下来,我们将尝试制作一个简单的塔防游戏。既然塔防游戏的三个要素是地图、敌人和防守单位,我们就从这三个方面入手。在本篇文章中,我们将运用以下知识:
- Unity2D 中的 Sprite 动画
- Unity3D 中的可视化辅助类 Gizmos
- 塔防游戏中敌人按路径寻路的实现
- Unity3D uGUI 的初步探索
- 简单的 AI 算法
地图篇
地图是塔防游戏中玩家关注的重点,因为地图和敌人的设置会直接影响玩家的策略。下图是博主从《保卫萝卜》游戏中提取的一张游戏地图,从中我们可以清晰看到怪物的进攻路径,它们会沿着地图中的路线向我方防守单位发起攻击。
那么,在游戏里如何确定怪物的攻击路径呢?首先对地图进行分析,地图中主要有两种类型的区域:可以放置防守单位的区域和不可放置防守单位的区域。基于此,我们可以设计如下结构:
using UnityEngine;
using System.Collections;
[SerializeField]
public class GridNode : MonoBehaviour
{
public enum NodeType
{
CanPlace,
CantPlace
}
public NodeType GridNodeType = NodeType.CanPlace;
}
在 GridNode 类中,我们定义了一个名为 NodeType 的枚举类型,它有两个值:CanPlace 表示可以放置防守单位,CantPlace 表示不可以放置。GridNode 类中只有一个 NodeType 类型的成员变量 GridNodeType,其默认值为 CanPlace。
现在我们找到了描述地图不同区域的方法,但还需要确定这些区域的位置。这时就需要用到 Gizmos 类,它是 Unity 中用于在场景视图进行可视化调试或辅助设置的工具类。当我们需要在编辑器环境中进行可视化调试时,就可以使用 Gizmos 类。所有的 Gizmo 绘制操作都需要在 OnDrawGizmos 或 OnDrawGizmosSelected 函数中完成。OnDrawGizmos 会在每一帧都被调用,所有在 Gizmos 里渲染的内容都会被显示;而 OnDrawGizmosSelected 仅在脚本附加的物体被选中时才会渲染。
了解了 Gizmos 的基本概念和用法后,我们回到游戏开发中。我们要生成区域以便使用 GridNode 类描述其属性,具体做法是在地图上绘制网格,将地图分割成不同区域,然后用 GridNode 描述每个区域的属性。以下是具体的脚本:
using UnityEngine;
using System.Collections;
public class GridMap : MonoBehaviour {
public static GridMap Instance = null;
public int MapSizeX;
public int MapSizeZ;
[HideInInspector]
public GameObject[] mNodes;
[HideInInspector]
public GameObject[] mPaths;
void Awake()
{
Instance = this;
mNodes = GameObject.FindGameObjectsWithTag("GridNode");
mPaths = GameObject.FindGameObjectsWithTag("PathNode");
}
void DrawGrid()
{
Gizmos.color = Color.blue;
for(int i = 0; i <= MapSizeX; i++)
{
Gizmos.DrawLine(new Vector3(i, 0, 0), new Vector3(i, MapSizeZ, 0));
}
for(int j = 0; j <= MapSizeZ; j++)
{
Gizmos.DrawLine(new Vector3(0, j, 0), new Vector3(MapSizeX, j, 0));
}
}
void DrawColor()
{
if(mNodes == null) return;
foreach(GameObject go in mNodes)
{
Vector3 mPos = go.transform.position;
if(go.GetComponent<GridNode>() != null){
if(go.GetComponent<GridNode>().GridNodeType == GridNode.NodeType.CanPlace){
Gizmos.color = Color.green;
} else if(go.GetComponent<GridNode>().GridNodeType == GridNode.NodeType.CantPlace){
Gizmos.color = Color.red;
}
Gizmos.DrawCube(mPos, new Vector3(1, 1, 1));
}
}
}
void DrawPath()
{
Gizmos.color = Color.white;
if(mPaths == null) return;
foreach(GameObject go in mPaths)
{
if(go.GetComponent<PathNode>() != null){
PathNode node = go.GetComponent<PathNode>();
if(node.ThatNode != null){
Gizmos.DrawLine(node.transform.position, node.ThatNode.transform.position);
}
Gizmos.DrawCube(node.transform.position, new Vector3(0.25F, 0.25F, 0.25F));
}
}
}
void OnDrawGizmos()
{
DrawGrid();
DrawColor();
DrawPath();
}
}
在这段脚本中,我们定义了两个 int 类型的变量 MapSizeX 和 MapSizeZ,用于表示需要绘制网格的大小。重点关注 OnDrawGizmos 方法,它调用了三个方法:DrawGrid、DrawColor 和 DrawPath。DrawGrid 方法负责绘制地图网格,从原点开始绘制交错的横线和竖线,使用了 Gizmos 类的 DrawLine 方法。
我们在场景中创建一个名为 MeshRoot 的空物体,并将 GridMap 脚本附加到该物体上。由于 Gizmos 提供了可视化调试功能,我们可以直接在编辑器窗口中看到绘制的地图网格效果。
为了让地图的左下角和场景原点完全匹配,博主编写了一个简单的工具类 AutoPlace 来实现地图的位置计算和调整:
using UnityEngine;
using System.Collections;
public class AutoPlace : MonoBehaviour {
// 精灵渲染器
private SpriteRenderer mRenderer;
// 精灵宽度
private float mSpriteWidth;
// 精灵高度
private float mSpriteHeight;
void Start ()
{
mRenderer = GetComponent<SpriteRenderer>();
// 计算精灵的实际大小
mSpriteWidth = mRenderer.sprite.bounds.size.x * transform.localScale.x;
mSpriteHeight = mRenderer.sprite.bounds.size.y * transform.localScale.y;
// 自动调整精灵的位置
transform.position = new Vector3(mSpriteWidth / 2, mSpriteHeight / 2, 0);
}
}
接下来讲解地图中区域的生成。在塔防游戏中,玩家通常只能在可放置防守单位的区域放置单位,这些区域就是我们要研究的对象。我们根据第一步绘制的网格,为每个网格单元创建一个空物体 NodeObject,并为其附加 GridNode 脚本。如果该物体所在位置可以放置防守单位,就将其 GridNodeType 设为 CanPlace,否则设为 CantPlace。这里为了便于讲解,我们采用手动创建的方式。我们需要给每个 NodeObject 设置一个 GridNode 的 Tag,以便在程序中通过 Tag 获取所有的 NodeObject。最后,将这些 NodeObject 作为子节点放到 MeshRoot 节点下。
回到 GridMap 脚本的 DrawColor 方法,在 Awake 方法中获取所有的 NodeObject,然后根据每个 NodeObject 附加的 GridNode 脚本判断该网格单元是否可以放置防守单位,若可以则用绿色绘制一个 Cube,否则用红色绘制。这样在编辑器中就可以通过颜色区分不同区域。
现在我们可以清楚地看到地图中区域的分布,红色部分为不可放置防守单位的区域,绿色部分为可放置区域。红色区域中的白色线就是敌人的寻路路径,接下来我们讲述敌人寻路路径的生成。
相比网格和区域的生成,路径的生成要简单一些,因为只需要关注起点、终点和节点。具体步骤如下:首先在场景中新建一个空物体,命名为 PathRoot;然后在红色区域中分别为起点、终点和节点创建空物体,命名为 PathNode,并设置其 Tag 为 PathNode。
下面是 PathNode 脚本,用于描述各个路径节点的关系,类似于链表结构:
using UnityEngine;
using System.Collections;
public class PathNode : MonoBehaviour {
public PathNode ThisNode;
public PathNode ThatNode;
public void SetNode(PathNode _node)
{
if(ThatNode != null){
ThatNode.ThisNode = null;
ThatNode = _node;
_node.ThisNode = this;
}
}
}
在这个脚本中,ThisNode 指向节点自身,ThatNode 指向下一个节点,并提供了 SetNode 方法用于设置下一个节点。我们将该脚本附加到各个 PathNode 上,通过编辑器可以快速指定每个节点的 ThisNode 和 ThatNode。
下面是 DrawPath 方法的具体实现:
void DrawPath()
{
Gizmos.color = Color.white;
if(mPaths == null) return;
foreach(GameObject go in mPaths)
{
if(go.GetComponent<PathNode>() != null){
PathNode node = go.GetComponent<PathNode>();
if(node.ThatNode != null){
Gizmos.DrawLine(node.transform.position, node.ThatNode.transform.position);
}
Gizmos.DrawCube(node.transform.position, new Vector3(0.25F, 0.25F, 0.25F));
}
}
}
该方法首先根据 Tag 获取所有的 PathNode 对象,然后根据 PathNode 脚本绘制每个节点指向下一个节点的线段,并为节点绘制一个小 Cube。
总结
到目前为止,关于地图的内容已经讲解完毕。在这一部分,我们主要学习了可视化辅助类 Gizmos 在绘制网格、区域、路径等方面的应用,主要使用了 DrawLine 和 DrawCube 这两个方法。
由于这个项目内容较多,博主决定将敌人篇、防守单位篇放在下一篇文章中讲解,这样既能减轻博主的写作负担,也能让大家阅读起来更加轻松。希望大家喜欢今天的内容!最后为大家送上今天的项目演示。