任务系统简析

任务系统简析

基本介绍

本项目中的任务系统总共分为两个部分:

  1. 基础部分:包含QuestManager.cs和QuestBase.cs两个脚本。
  2. 特例部分:指的是任务系统中自带的典型任务模板。例如:Trigger Quest模板、Fire Quest模板和Time Quest模板。
    • Trigger Quest:用于判断一个物体是否到达指定区域的任务。通常可以用于判断玩家是否成功解救了被困人员,把他们接送到指定位置。
    • Fire Quest:用于判断一个区域的火焰是否熄灭的任务。可以用于判断玩家是否成功扑灭指定区域的所有火焰。
    • Time Quest:用于倒计时的任务。可以用于判断规定时间内某个任务是否完成。
    • Other Quest:此任务为空白,用于后续拓展。

(在结尾有需要注意的地方提示。)

基础部分

QuestBase.cs

此脚本是所有任务的基类,包含一个任务该有的基本属性和一些虚函数。

Quest ID

1
2
3
4
5
6
7
8
9
10
11
12
private string _QuestID;
public string QuestID
{
get
{
return _QuestID;
}
set
{
_QuestID = value;
}
}

QuestID是用来标识每一个quest的整数,在任务需要被寻找或者操作的时候,需要用到ID。

(虽然说,现在并没有用到,但后续拓展可能会用。)

基本属性

1
public QuestManager.QuestState.State state;

任务的基本属性为任务状态:根据状态来对任务进行操作。

虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public virtual void OnQuestStart()
{
Debug.Log("Quest start.");
}

public virtual void QuestDetect()
{
Debug.Log("Is detecting quest...");
}

public virtual void OnQuestFinished()
{
Debug.Log("Quest finished.");
}

在每一个具体的Quest中,需要去重写这些方法。

QuestManager.cs

此脚本用于管理所有的Quest,当运行场景时,会创造一个Game Object Quest Manager单例来进行管理。其中包含了所有的Quest State(任务状态),Quest Type(任务种类)以及一些任务初始化的方法。

创建单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// singleton
private static QuestManager instance = null;
public static QuestManager Instance
{
get
{
if(instance == null)
{
GameObject go = new GameObject("QuestManager");
instance = go.AddComponent<QuestManager>();
}
return instance;
}
}

任务状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Quest状态类
public class QuestState
{
public enum State
{
Unprepared,
Prepared,
Running,
Finished,
End
}

public State state;
}

这里详细说明一下一个任务从创建到结束的生命周期,可以用下面这张图来表示:

任务状态图

  • 当你创建一个任务时,任务的基本状态会是Unprepared,这意味着你除了更改它的基本属性之外,无法对它进行任何操作,它也不会对正在进行中的项目有任何影响。
  • 当任务状态为Unprepared时,它只会做一件事:检测任务的开始条件是否满足。任务的开始条件时它的前置任务队列清空。此时任务就会开始准备运行。它的状态也变为Prepared。
  • 一般来说,在本项目中,一个任务一旦准备好,就立刻开始运行,基本不会有任务需要在运行途中手动设置该任务是否开始运行。所以它的状态会立刻变为Running。如果有例外的任务出现,那么增加一个bool变量控制即可。
  • 任务开始运行以后,玩家就要进行一系列操作使得任务完成或者失败。(暂时默认成功,因为失败的话要和评分系统进行对接,还没整)任务完成后,状态变为Finished。
  • 此时任务结束了,那么需要做的就是根据这个任务的后置任务列表,去找对应的任务前置列表中的这个任务,并把它删除。完成后任务状态变为End。
  • 当任务状态为End时,任务就真正结束了。你无法对它进行任何操作。

任务种类

1
2
3
4
5
6
7
8
9
10
// QuestType 类
public class QuestType
{
public enum Type
{
TriggerQuest,
TimeQuest,
ParticleQuest
}
}

这里写出了任务的特殊种类。(虽然现在写文档好像发现并没有用到这个,但是写出来还是会方便阅读一点。)

创建一个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public QuestBase CreateQuest(int type_number)
{
switch (type_number)
{
case 0:
Debug.Log("Create a TriggerQuest");
QuestBase trigger_quest = new TriggerQuest();
InitQuest(trigger_quest);
return trigger_quest;

case 1:
Debug.Log("Create a TimeQuest");
QuestBase time_quest = new TimeQuest();
InitQuest(time_quest);
return time_quest;

case 2:
Debug.Log("Create a ParticleQuest");
QuestBase particle_quest = new ParticleQuest();
InitQuest(particle_quest);
return particle_quest;

default:
Debug.Log("There is no ideal quest. Please Create a new quest type and start programming it.");
QuestBase other_quest = new OtherQuest();
return other_quest;
}
}

使用对应的Quest number来创建具体任务。0对应Trigger quest,1对应Time quest, 2对应Particle quest(也就是Fire quest, 取名是因为粒子系统)。

如果要输入其他数字的话,都会报错并提醒你去新建一个quest种类。

初始化任务

1
2
3
4
5
public void InitQuest(QuestBase quest)
{
quest.state = QuestState.State.Unprepared;
Debug.Log(quest.state);
}

这里初始化了一个任务的状态。把任务刚开始的状态设置为Unprepared。

特例部分

特例部分主要分为任务系统内部自带的几个任务类型:暂时有Trigger Quest、Fire Quest和Time Quest。

下面对于每个任务类型,都将进行代码的简要说明,并且运用示例来说明如何创建一个对应的任务。

Trigger Quest

Trigger Quest文件夹内有两个代码文件:

  1. Particle Quest:重写三个Quest Base虚函数。
  2. Particle Test:示例脚本。

Trigger Quest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TriggerQuest : QuestBase
{
public override void OnQuestStart()
{
state = QuestManager.QuestState.State.Prepared;
Debug.Log(state);
Debug.Log("Trigger Quest is prepared.");
base.OnQuestStart();
state = QuestManager.QuestState.State.Running;
Debug.Log(state);
}
public override void QuestDetect()
{
if(state == QuestManager.QuestState.State.Running)
{
Debug.Log("Trigger Quest is being detected.");
base.QuestDetect();
}
}
public override void OnQuestFinished()
{
if(state == QuestManager.QuestState.State.Finished)
{
Debug.Log("Trigger Quest is finished.");
base.OnQuestFinished();
state = QuestManager.QuestState.State.End;
Debug.Log(state);
}
}
}

Trigger Quest是一个继承于Quest Base的类,其中主要作用是重写了三个虚函数,然后在里面改变任务的状态。

Trigger Quest示例

假设我们有这样一个场景:

image-20211003190852197

我们的任务需求是:让红色的方块落在右边的白色区域内。如果落在右边的区域内,那么就算任务成功,如果落在左边(其他)区域内,就算任务失败(此时任务也不会终止,而是继续检测)。

那我们就可以写一个脚本来创建任务,并挂在对应的任务物体上。(脚本内容直接用注释标明了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class TriggerTest : MonoBehaviour
{
private QuestBase trigger_quest;
public Collider target_collider;//这是一个需要手动配置的collider,把对应的区域collider拖进去

//这是这个任务的前置列表,是一个bool型的委托
public delegate bool PreQuestDelegate();
public PreQuestDelegate pre_quest;

private void Start()
{
trigger_quest = QuestManager.Instance.CreateQuest(0);
//此处就是添加这个任务的前置列表内容,找到前置任务挂载的物体,在找到它的脚本,调用对应的函数。
//这里我把前置任务添加为三个灭火的任务。
pre_quest += GameObject.Find("FireObject").GetComponent<ParticleTest>().AddQuest;
pre_quest += GameObject.Find("FireObject1").GetComponent<ParticleTest1>().AddQuest;
pre_quest += GameObject.Find("FireObject2").GetComponent<ParticleTest2>().AddQuest;
Debug.Log(pre_quest());
//当所有的前置任务调用函数return true的时候,pre_quest才会为真,这个任务才开始启动。
if (pre_quest())
{
trigger_quest.OnQuestStart();
trigger_quest.QuestDetect();
}
}

private void OnCollisionEnter(Collision collision)
{
if (trigger_quest != null)
{
//判断这个任务是否是开始状态
if (trigger_quest.state == QuestManager.QuestState.State.Running)
{
//判断是否是对应的区域
if (collision.collider != target_collider)
{
Debug.Log("Not the target gameobject. Continue.");
}
else
{
Debug.Log("Object is in the target area.");
trigger_quest.state = QuestManager.QuestState.State.Finished;
Debug.Log(trigger_quest.state);
trigger_quest.OnQuestFinished();
}
}
}
}
}

在写完这个脚本后,我们需要:

  1. 把这个脚本挂在红色方块上。
  2. 把对应的区域拖到脚本里的空白处。
  3. 保存运行即可。

Fire Quest

Fire Quest文件夹内有三个代码文件:

  1. Fire Manager:管理一个区域里的火焰列表。
  2. Particle Quest:重写三个Quest Base虚函数。
  3. Particle Test:示例脚本。

Particle Quest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ParticleQuest : QuestBase
{
public override void OnQuestStart()
{
state = QuestManager.QuestState.State.Prepared;
Debug.Log(state);
Debug.Log("Particle Quest is prepared.");
base.OnQuestStart();
state = QuestManager.QuestState.State.Running;
Debug.Log(state);
}
public override void QuestDetect()
{
if (state == QuestManager.QuestState.State.Running)
{
Debug.Log("Particle Quest is being detected.");
base.QuestDetect();
}
}
public override void OnQuestFinished()
{
if (state == QuestManager.QuestState.State.Finished)
{
Debug.Log("Particle Quest is finished.");
base.OnQuestFinished();
state = QuestManager.QuestState.State.End;
Debug.Log(state);
}
}
}

Particle Quest是一个继承于Quest Base的类,其中主要作用是重写了三个虚函数,然后在里面改变任务的状态。

Fire Quest示例

假设我们有这样一个场景:

image-20211003200141822

我们的任务需求是:在这一个区域内有三个物体,他们都会着火。我们的目标是把这三处火全部扑灭,任务才算成功,否则任务失败。

那么首先,我们需要在创建一个空的GameObject,命名为FireManager,他会来管理这一块区域的火焰。我们把Fire Manager脚本挂到他身上,在列表里配置区域火焰。如下:

image-20211003202208840

然后我们需要把这三个物体转换为着火物体并进行配置。(具体看另一篇如何转换火焰物体的文档。)

接下来我们把Particle Test脚本分别挂在每一个物体上,也就意味着我们创建了三个子任务。而这三个子任务构成了一个大的灭火任务。

下面是代码和注释(相同的部分就不注释了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ParticleTest : MonoBehaviour
{
private QuestBase particle_quest;
private Ignis.FlammableObject flamObj;
private List<Ignis.FlammableObject> FireList;
public FireManager fire_manager;//需要把创建的fire manager拖到每一个物体上

private void Start()
{
FireList = fire_manager.FlamObjLists;
//Debug.Log(FireList[0]);
//Debug.Log(FireList[1]);
//Debug.Log(FireList[2]);
flamObj = GetComponent<Ignis.FlammableObject>();//获得fire manager的列表
particle_quest = QuestManager.Instance.CreateQuest(2);//创建一个子任务
particle_quest.OnQuestStart();
particle_quest.QuestDetect();
}

private void OnParticleCollision(GameObject other)
{
if (particle_quest != null)
{
if (particle_quest.state == QuestManager.QuestState.State.Running)
{
//Debug.Log(flamObj.extinguished);
if (flamObj.extinguished)//判断该着火物体是否已经被扑灭了,按照正常的逻辑来讲,应该是一个物体上没火了火才算被扑灭,但是由于插件自身的原因,它在显示被扑灭后还会有一点火焰在逐渐熄灭,所以有的物体会出现任务完成了但是火还在烧,这属于正常现象,因为过一会它就自行熄灭了。
{
Debug.Log("Fire ends");
particle_quest.state = QuestManager.QuestState.State.Finished;
particle_quest.OnQuestFinished();
if (particle_quest.state == QuestManager.QuestState.State.End)
{
FireList.Remove(flamObj);//把这个物体从火焰列表里面移除
Debug.Log("This fire is been removed from the list.");
if (FireList.Count == 0)//如果火焰列表已经为空,那么就这个区域的火就算都被扑灭了,任务完成。
{
Debug.Log("This area fire ends.");
}
}
}
}
}
}

public bool AddQuest()//这部分就是在之前的Trigger Quest中提到的前置任务的添加函数
{
Debug.Log("Add fire quest.");
return true;
}
}

在写完这个脚本后,我们需要:

  1. 先创建一个Fire Manager(如果一个区域内有多个物体会着火)
  2. 把对应的区域内物体拖到Fire Manager里的空白处。
  3. 把这个脚本挂在对应着火物体上。
  4. 保存运行即可。

(注意:既然是灭火,那你肯定还需要一个水的particle system才能完成交互灭火的操作,如何创建一个“水流”,请参考火焰插件使用说明的文档。)

Time Quest

Time Quest文件夹内有两个代码文件:

  1. Time Quest:重写三个Quest Base虚函数。
  2. Time Quest Test:示例脚本。

Time Quest

和之前的Quest不同,Time Quest.cs里会直接开始一个time manager,由它来启动计时。(具体用法看计时器的使用文档)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TimeQuest : QuestBase
{
TimeManager time_manager;

public override void OnQuestStart()
{
time_manager = TimeManager.Instance;
state = QuestManager.QuestState.State.Prepared;
Debug.Log(state);
Debug.Log("Time Quest is prepared.");
base.OnQuestStart();
time_manager.StartTimer();//开始计时
state = QuestManager.QuestState.State.Running;
Debug.Log(state);
}

public override void QuestDetect()
{
if (state == QuestManager.QuestState.State.Running)
{
Debug.Log("Time Quest is being detected.");
base.QuestDetect();

}
}
public override void OnQuestFinished()
{
if (state == QuestManager.QuestState.State.Finished)
{
Debug.Log("Time Quest is finished.");
base.OnQuestFinished();
state = QuestManager.QuestState.State.End;
Debug.Log(state);
}
}
}

Time Quest示例

假设我们需要一个任务要在3s内完成,那么我们就可以写如下脚本来创建这个任务。

Time Quest Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TimeQuestTest : MonoBehaviour
{
QuestBase time_quest;
TimeManager time_manager;

public float time = 3.0f;//在外面可以人为设定

void Start()
{
time_manager = TimeManager.Instance;
time_quest = QuestManager.Instance.CreateQuest(1);

time_quest.OnQuestStart();
time_manager.InitializeTimer(time, IsEnd);//调用计时器的方法。
time_quest.QuestDetect();
}

public void IsEnd()
{
Debug.Log("This time quest is end.");
time_quest.state = QuestManager.QuestState.State.Finished;
time_quest.OnQuestFinished();
}
}

在写完这个脚本后,我们需要:

  1. 把这个脚本挂在一个GameObject上。
  2. 设定自定义的时间。
  3. 保存运行即可。

需要注意的地方

在创建一个任务时,你要想到以下几件事:

  1. 我要创建一个什么任务?它的类型是什么?
  2. 它会不会和别的任务有关联?它是否必须要在哪些任务之后进行?哪些任务又必须在它之后才能开始?
  3. 任务开始时会发生什么?
  4. 如何判断这个任务结束?
  5. 任务结束以后会发生什么?

根据这些去写对应的代码脚本,挂在对应的物体上即可。

一个完整的任务流程一个就包括之前的几个问题,之前的示例脚本并没有每一个都包含,只是简单的示例而已,在实际任务创建时,需要具体任务具体分析,借鉴示例中的方式进行操作。