unity3d做塔防游戏

2015年01月28日 11:08 0 点赞 1 评论 更新于 2017-05-08 07:53

今天我们将学习如何使用 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 绘制操作都需要在 OnDrawGizmosOnDrawGizmosSelected 函数中完成。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 类型的变量 MapSizeXMapSizeZ,用于表示需要绘制网格的大小。重点关注 OnDrawGizmos 方法,它调用了三个方法:DrawGridDrawColorDrawPathDrawGrid 方法负责绘制地图网格,从原点开始绘制交错的横线和竖线,使用了 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 设置一个 GridNodeTag,以便在程序中通过 Tag 获取所有的 NodeObject。最后,将这些 NodeObject 作为子节点放到 MeshRoot 节点下。

回到 GridMap 脚本的 DrawColor 方法,在 Awake 方法中获取所有的 NodeObject,然后根据每个 NodeObject 附加的 GridNode 脚本判断该网格单元是否可以放置防守单位,若可以则用绿色绘制一个 Cube,否则用红色绘制。这样在编辑器中就可以通过颜色区分不同区域。

现在我们可以清楚地看到地图中区域的分布,红色部分为不可放置防守单位的区域,绿色部分为可放置区域。红色区域中的白色线就是敌人的寻路路径,接下来我们讲述敌人寻路路径的生成。

相比网格和区域的生成,路径的生成要简单一些,因为只需要关注起点、终点和节点。具体步骤如下:首先在场景中新建一个空物体,命名为 PathRoot;然后在红色区域中分别为起点、终点和节点创建空物体,命名为 PathNode,并设置其 TagPathNode

下面是 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 上,通过编辑器可以快速指定每个节点的 ThisNodeThatNode

下面是 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 在绘制网格、区域、路径等方面的应用,主要使用了 DrawLineDrawCube 这两个方法。

由于这个项目内容较多,博主决定将敌人篇、防守单位篇放在下一篇文章中讲解,这样既能减轻博主的写作负担,也能让大家阅读起来更加轻松。希望大家喜欢今天的内容!最后为大家送上今天的项目演示。

作者信息

feifeila

feifeila

共发布了 570 篇文章