Unity3D游戏开发之快速打造流行的关卡系统

2016年11月02日 16:02 0 点赞 1 评论 更新于 2025-11-21 20:45
Unity3D游戏开发之快速打造流行的关卡系统

今天,我将和大家分享目前在移动平台上较为流行的关卡系统。关卡系统通常是单机手机游戏,如《愤怒的小鸟》《保卫萝卜》中组织游戏内容的形式。玩家可以通过已解锁的关卡(默认第一关已解锁)获取分数来解锁新关卡,也可以付费购买解锁。在本文中,我将带领大家快速实现一个可扩展的关卡系统,该实例的灵感源于我最近的工作经历,希望能对大家学习Unity3D游戏开发有所帮助。

原理

在本地配置一个Xml文件,在其中定义当前游戏中关卡的相关信息。通过解析该文件并将其与UI绑定,最终实现一个完整的关卡系统。

1. 定义关卡

首先,我们来定义一个关卡的基本结构:

public class Level
{
/// <summary>
/// 关卡ID
/// </summary>
public string ID;

/// <summary>
/// 关卡名称
/// </summary>
public string Name;

/// <summary>
/// 关卡是否解锁
/// </summary>
public bool UnLock = false;
}

在这里,我们假定关卡的名称和该关卡在Unity3D中场景的名称一致。其中,UnLock属性是一个布尔型变量,它表明该关卡是否解锁。因为在游戏中,只有解锁的场景才可以被访问。

2. 定义关卡配置文件

根据关卡的基本结构Level,我们可以定义如下的配置文件。这里使用Xml作为配置文件的存储形式:

<?xml version="1.0" encoding="utf-8"?>
<levels>
<level id="0" name="level0" unlock="1" />
<level id="1" name="level1" unlock="0" />
<level id="2" name="level2" unlock="0" />
<level id="3" name="level3" unlock="0" />
<level id="4" name="level4" unlock="0" />
<level id="5" name="level5" unlock="0" />
<level id="6" name="level6" unlock="0" />
<level id="7" name="level7" unlock="0" />
<level id="8" name="level8" unlock="0" />
<level id="9" name="level9" unlock="0" />
</levels>

和关卡结构定义类似,这里使用01来表示关卡的解锁情况,0表示未解锁,1表示解锁。可以注意到,默认情况下第一个关卡是解锁的,这与我们玩《愤怒的小鸟》这类游戏时的直观感受相符。

在完成了关卡的结构定义和配置文件定义后,接下来我们思考如何实现一个关卡系统。由于此处不涉及Unity3D场景中的具体逻辑,因此在关卡系统中,我们主要的工作是维护好主界面场景和各个游戏场景的跳转关系。具体来说,我们需要完成两件事情:

  • 第一,将配置文件中的关卡以一定形式加载到主界面中,并告知玩家哪些关卡已解锁、哪些关卡未解锁。当玩家点击不同的关卡时,应得到不同的响应。已解锁的关卡可以访问并进入游戏环节,未解锁的关卡则需要获得更多分数或付费来解锁。
  • 第二,对关卡进行编辑。当玩家获得分数或支付一定费用后,可以解锁关卡并进入游戏环节。

综合这两点来看,我们需要对关卡的配置文件进行读写操作。因为一个关卡是否解锁仅取决于unlock属性,明白了这一点后,我们来编写一个维护关卡的类。

3. 编写一个维护关卡的类

以下是维护关卡的类的代码:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;

public static class LevelSystem
{
/// <summary>
/// 加载Xml文件
/// </summary>
/// <returns>关卡列表</returns>
public static List<Level> LoadLevels()
{
// 创建Xml对象
XmlDocument xmlDoc = new XmlDocument();
// 如果本地存在配置文件则读取配置文件
// 否则在本地创建配置文件的副本
// 为了跨平台及可读可写,需要使用Application.persistentDataPath
string filePath = Application.persistentDataPath + "/levels.xml";
if (!IOUtility.IsFileExists(filePath))
{
xmlDoc.LoadXml(((TextAsset)Resources.Load("levels")).text);
IOUtility.CreateFile(filePath, xmlDoc.InnerXml);
}
else
{
xmlDoc.Load(filePath);
}
XmlElement root = xmlDoc.DocumentElement;
XmlNodeList levelsNode = root.SelectNodes("/levels/level");
// 初始化关卡列表
List<Level> levels = new List<Level>();
foreach (XmlElement xe in levelsNode)
{
Level l = new Level();
l.ID = xe.GetAttribute("id");
l.Name = xe.GetAttribute("name");
// 使用unlock属性来标识当前关卡是否解锁
if (xe.GetAttribute("unlock") == "1")
{
l.UnLock = true;
}
else
{
l.UnLock = false;
}
levels.Add(l);
}
return levels;
}

/// <summary>
/// 设置某一关卡的状态
/// </summary>
/// <param name="name">关卡名称</param>
/// <param name="unlock">是否解锁</param>
public static void SetLevels(string name, bool unlock)
{
// 创建Xml对象
XmlDocument xmlDoc = new XmlDocument();
string filePath = Application.persistentDataPath + "/levels.xml";
xmlDoc.Load(filePath);
XmlElement root = xmlDoc.DocumentElement;
XmlNodeList levelsNode = root.SelectNodes("/levels/level");
foreach (XmlElement xe in levelsNode)
{
// 根据名称找到对应的关卡
if (xe.GetAttribute("name") == name)
{
// 根据unlock重新为关卡赋值
if (unlock)
{
xe.SetAttribute("unlock", "1");
}
else
{
xe.SetAttribute("unlock", "0");
}
}
}
// 保存文件
xmlDoc.Save(filePath);
}
}

这里我们将关卡配置文件levels.xml放置在Resources目录下,因为使用Resources.Load()方式加载本地资源对Unity3D有以下优势:

  • 它使用相对于Resources目录的相对路径,使用时无需考虑是相对路径还是绝对路径的问题。
  • 它使用名称来查找本地资源,使用时无需考虑扩展名和文件格式的问题。
  • 它可以是Unity3D支持的任意类型,从贴图到预制体再到文本文件等,能与Unity3D的API完美结合。

不过,Resources.Load()也有缺点,即不支持写入操作。这并非Unity3D的问题,因为Unity3D导出游戏时会将Resources目录下的内容压缩后再导出,我们不能要求在压缩后的文件里支持写入操作。下面总结一下Unity3D中常见的资源读写方案:

  1. Resources.Load:只读。当资源不需要更新且对本地存储无容量要求时可采用此方式。
  2. AssetBundle:只读。当资源需要更新且对本地存储有容量要求时可采用此方式。
  3. WWW:只读。WWW支持http协议和file协议,可用于加载网络资源或本地资源。
  4. PlayerPrefs:可读可写。Unity3D提供的一种简单的键 - 值型存储结构,可用于读写floatintstring三种简单的数据类型,是一种较为松散的数据存储方案。
  5. 序列化和反序列化:可读可写。可使用Protobuf,将数据序列化为Xml、二进制或JSON等形式实现资源读写。
  6. 数据库:可读可写。可使用MySQLSQLite等数据库对数据进行存储以实现资源读写。

了解了Unity3D中资源读写的常见方案后,我们再来讨论一下Unity3D中的路径问题。Application.dataPath是我们常用的一个路径,但它在不同平台下是不一样的,从官方API文档可知,该值依赖于运行的平台:

  • Unity编辑器:<工程文件夹的路径>/Assets
  • Mac:<到播放器应用的路径>/Contents
  • IOS:<到播放器应用的路径>/

4. 编写入口文件

以下是入口文件的代码:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using System.Xml.Serialization;

public class Main : MonoBehaviour
{
// 关卡列表
private List<Level> m_levels;

void Start ()
{
// 获取关卡
m_levels = LevelSystem.LoadLevels();
// 动态生成关卡
foreach (Level l in m_levels)
{
GameObject prefab = (GameObject)Instantiate((Resources.Load("Level") as GameObject));
// 数据绑定
DataBind(prefab, l);
// 设置父物体
prefab.transform.SetParent(GameObject.Find("UIRoot/Background/LevelPanel").transform);
prefab.transform.localPosition = new Vector3(0, 0, 0);
prefab.transform.localScale = new Vector3(1, 1, 1);
// 将关卡信息传给关卡
prefab.GetComponent<LevelEvent>().level = l;
prefab.name = "Level";
}
// 人为解锁第二个关卡
// 在实际游戏中玩家需要满足一定条件方可解锁关卡
// 此处仅作为演示
LevelSystem.SetLevels("level1", true);
}

/// <summary>
/// 数据绑定
/// </summary>
void DataBind(GameObject go, Level level)
{
// 为关卡绑定关卡名称
go.transform.Find("LevelName").GetComponent<Text>().text = level.Name;
// 为关卡绑定关卡图片
Texture2D tex2D;
if (level.UnLock)
{
tex2D = Resources.Load("nolocked") as Texture2D;
}
else
{
tex2D = Resources.Load("locked") as Texture2D;
}
Sprite sprite = Sprite.Create(tex2D, new Rect(0, 0, tex2D.width, tex2D.height), new Vector2(0.5F, 0.5F));
go.transform.GetComponent<Image>().sprite = sprite;
}
}

在这段脚本中,我们首先加载了关卡信息,然后将关卡信息与界面元素绑定,实现了一个简单的关卡选择界面,并人为地解锁了第二个关卡。这里的UI基于UGUI实现。为了让每个关卡的UI元素能响应事件,我们需要编写一个LevelEvent脚本:

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class LevelEvent : MonoBehaviour
{
// 当前关卡
public Level level;

public void OnClick()
{
if (level.UnLock)
{
// 假设关卡的名称即为对应场景的名称
// Application.LoadLevel(level.Name);
Debug.Log("当前选择的关卡是:" + level.Name);
}
else
{
Debug.Log("抱歉!当前关卡尚未解锁!");
}
}
}

在本文开始时,我提到了一个假设,即关卡的名称和其对应的游戏场景名称一致。现在大家应该明白原因了,为了让每个关卡的UI元素知道自己对应哪个关卡,我们设置了一个level变量,该变量的值在加载关卡时已完成初始化,这样我们就能知道每个关卡的具体信息,从而完成事件的响应。

最后,我们来看一下最终的效果。可以注意到,在第二次打开游戏后,第二个关卡已经解锁了,这说明我们最初设计的两个目标都已达成。如果大家有好的想法或建议,欢迎在文章后面评论。

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章