Daniel's Studio.

3D-Game-Programming-Design-游戏对象与图形基础

字数统计: 2.4k阅读时长: 10 min
2019/09/22 Share

3D游戏编程的第四次作业:

  • 牧师与恶魔 - 动作分离版

课程主页

基本操作演练

下载 Fantasy Skybox FREE, 构建自己的游戏场景

从Asset Store中搜索并下载Fantasy Skybox FREE后,其中包含了好几个skybox,用于设置游戏界面的背景。同时还有一些树的预制Prefabs,地面图片等等,可以用于制作地形Terrain。但由于这个资源是免费的,个人感觉并不是很好看。

写一个简单的总结,总结游戏对象的使用

目前为止,我对于游戏对象的使用还只是停留在最基本的操作上:移动,缩放,旋转等等,并可以通过编写脚本来对于游戏对象进行加载和控制,来达到一些简单的游戏功能。

此外还因为从Asset Store中下载的模型自带动作,我简单的尝试了对于游戏对象动作的控制,关键在于设定具体的状态,将动作与状态绑定,并设置状态之间的转移。

编程实践

牧师与魔鬼 动作分离版

  • 【2019新要求】:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束

项目地址

由于只是变更了代码结构,实际游戏效果并没有改变,所以视频沿用上一次的:演示视频地址

上一次的作业中,对于牧师与魔鬼这个游戏,我们采用了MVC的设计架构,使得程序在一定程度上解耦,每一层可以专注于自己的业务逻辑。但是,在实践中发现,即使对于这样一个简单的小游戏,在控制器层所要做的事情还是太多了,既要负责加载资源,同时要处理模型间很多动作的执行与判断,导致代码比较冗长。

因此,本次作业中我们将写一个动作管理器,负责单独管理动作。除此之外,还将游戏结束判断的代码也提取了出来,写了一个裁判类。

以下是动作管理器的UML类图:

基于上述类,修改游戏的脚本结构如下:

下面简单介绍一些这些类的关系及其职责:

SSAction

SSAction是所有动作的基类。包含了一些所有动作都会用到的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destory = false;

public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }

protected SSAction() { }

public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
SSActionCallback

类似于回调函数,作为动作和动作管理器交流的一种方式。我们可以看到,在动作基类中,存在ISSActionCallbcak属性,而动作管理器类会继承这个接口,并实现其方法。每当动作管理器执行某个动作时,会把该动作的这个属性设为自己。因此,当动作完成后,就可以通过callback.SSActionEvent这个函数通知管理器。

1
2
3
4
5
6
7
8
9
10
public enum SSActionEventType : int { Started, Compeleted }

public interface ISSActionCallback
{
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Compeleted,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
CCMoveToAction

简单的移动,从上次作业的代码中提取出的

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 CCMoveToAction : SSAction
{
public Vector3 target;
public float speed;

public static CCMoveToAction GetSSAction(Vector3 target, float speed)
{
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}

// Use this for initialization
public override void Start()
{

}

// Update is called once per frame
public override void Update()
{
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
if(this.transform.position == target)
{
this.destory = true;
this.callback.SSActionEvent(this);
}
}
}
CCSequenceAction

这个类用于实现一系列连续的动作,通过List保存所要执行的一系列动作,采用了门面设计模式,将连续的动作封装。因此,它有着动作管理器的效果,所以也实现了ISSActionCallback接口。当一系列动作完成后,先通知这个类,然后这个类通知更高一级的管理器。

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
52
53
54
55
56
57
58
59
60
61
62
63
public class CCSequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1;
public int currentIndex = 0;

public static CCSequenceAction GetSSAction(int repeat, int currentIndex, List<SSAction> sequence)
{
CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
action.repeat = repeat;
action.currentIndex = currentIndex;
action.sequence = sequence;
return action;
}

// 执行动作前,为每个动作注入当前动作游戏对象,并将自己作为动作事件的接收者
public override void Start()
{
foreach(SSAction action in sequence)
{
action.gameObject = this.gameObject;
action.transform = this.transform;
action.callback = this;
action.Start();
}
}

// Update is called once per frame
public override void Update()
{
if (sequence.Count == 0)
return;
if(currentIndex < sequence.Count)
{
sequence[currentIndex].Update();
}
}

public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Compeleted, int intParam = 0, string strParam = null, Object objectParam = null)
{
source.destory = false;
this.currentIndex++;
if(this.currentIndex >= sequence.Count)
{
this.currentIndex = 0;
if (repeat > 0)
repeat--;
if(repeat == 0)
{
this.destory = true;
this.callback.SSActionEvent(this);
}
}
}

private void OnDestroy()
{
foreach(SSAction action in sequence)
{
Object.Destroy(action);
}
}
}
SSActionManager

动作管理器基类,实现了动作管理器的核心功能。在每次Update时,检查所有动作,通过两个队列和一个字典数据结构完成当前所有动作的添加、执行、销毁操作。

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
52
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();


// Use this for initialization
protected void Start()
{

}

// Update is called once per frame
protected void Update()
{
foreach (SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();

foreach(KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destory)
{
waitingDelete.Add(ac.GetInstanceID());
}else if (ac.enable)
{
ac.Update();
}
}

foreach(int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
waitingDelete.Clear();
}

public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
{
action.gameObject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
}
CCActionManager

具体的动作管理器,其中Update函数沿用父类的,并定义了具体的动作和回调函数。通过父类的RunAction为动作设置对象等信息,并将动作加入等待队列。

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
public class CCActionManager : SSActionManager, ISSActionCallback
{
public FirstController sceneController;
public CCMoveToAction moveBoat;
public CCSequenceAction moveRole; //移动角色是一个组合动作
// Use this for initialization
protected new void Start()
{
sceneController = SSDirector.GetInstance().CurrentSceneController as FirstController;
sceneController.actionManager = this;
}

// Update is called once per frame
protected new void Update()
{
base.Update();
}

public void MoveBoat(GameObject boat, Vector3 target, float speed)
{
moveBoat = CCMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, moveBoat, this);
sceneController.moving = true;
}

public void MoveRole(GameObject role, Vector3 middlePosition, Vector3 endPosition, float speed)
{
SSAction step1 = CCMoveToAction.GetSSAction(middlePosition, speed);
SSAction step2 = CCMoveToAction.GetSSAction(endPosition, speed);
moveRole = CCSequenceAction.GetSSAction(1, 0, new List<SSAction> { step1, step2 });
this.RunAction(role, moveRole, this);
sceneController.moving = true;
}

#region ISSActionCallback implementation
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Compeleted,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
//回调函数,动作执行完后调用
sceneController.moving = false;

}
#endregion
}

以上就是动作管理器的全部类,在完成这些类后,需要对于之前的代码进行修改,最主要的是以下函数,其余细节部分此处不再赘述,具体可以见项目地址。

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
 public void MoveRole(RoleModel role)
{
if (!gaming || moving)
return;
Vector3 endPosition;

if (role.IsOnBoat()) //上岸
{
//这里,为了和原游戏一致,使牧师和魔鬼在两边的排列顺序一致
role = Boat.DeletePassenger(role);
int id = role.GetName()[role.GetName().Length - 1] - '0';
if (role.IsGood())
{
endPosition = PrisetsOriginPositions[id];
}
else
{
endPosition = DevilsOriginPositions[id];
}
if(role.GetSide() == -1)
{
endPosition.x = 0 - endPosition.x;
}

Vector3 middlePosition = new Vector3(role.GetRole().transform.position.x, endPosition.y, endPosition.z);
actionManager.MoveRole(role.GetRole(), middlePosition, endPosition, speed);
role.GoLand();
}
else //上船
{
if (Boat.IsFull() || Boat.GetSide() != role.GetSide())
{
return;

}
endPosition = Boat.getEmptyPosition();
Vector3 middlePosition = new Vector3(endPosition.x, role.GetRole().transform.position.y, endPosition.z);
actionManager.MoveRole(role.GetRole(), middlePosition, endPosition, speed);
role.GoBoat(Boat);
Boat.AddPassenger(role);
}
}
public void MoveBoat()
{
//当船为空,或者船或人物在运动时,不允许移动船
if (Boat.IsEmpty() || moving)
return;
actionManager.MoveBoat(Boat.GetBoat(), Boat.GetMoveDirection(), speed);
}
}

可以看到,真正动作的执行代码已经很少,全部交由actionManager完成。

裁判类

裁判类比较简单,只是把之前用于判断结果的代码单独写成一个类即可。我采用了让其在每次Update的时候进行判断,似乎频率有点过高,应该有更好的办法,但对于这个小游戏还是没有影响的。以下是裁判类代码:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Judger : MonoBehaviour
{
public FirstController sceneController;
// Use this for initialization
public void Start()
{
sceneController = SSDirector.GetInstance().CurrentSceneController as FirstController;
}

// Update is called once per frame
public void Update()
{
if((sceneController.currentState = Judge()) != 0)
{
sceneController.gaming = false;

}
}

public int Judge()
{
if (sceneController.moving)
return 0;
//计算两边的牧师和恶魔数量
int rightPriestNum = 0, leftPriestNum = 0, rightDevilNum = 0, leftDevilNum = 0;
for (int i = 0; i < 3; i++)
{
if (sceneController.Priests[i].GetSide() == 1)
{
rightPriestNum++;
}
else
{
leftPriestNum++;
}

if (sceneController.Devils[i].GetSide() == 1)
{
rightDevilNum++;
}
else
{
leftDevilNum++;
}
}
if (leftPriestNum + leftDevilNum == 6)
{
for (int i = 0; i < 3; i++)
{
sceneController.Devils[i].Lose();
}
return 1; //win
}
else if ((leftPriestNum > 0 && leftDevilNum > leftPriestNum) || (rightPriestNum > 0 && rightDevilNum > rightPriestNum))
{
int attackSide;
if (leftDevilNum > leftPriestNum)
attackSide = -1;
else
attackSide = 1;
for (int i = 0; i < 3; i++)
{
if (sceneController.Devils[i].GetSide() == attackSide)
sceneController.Devils[i].Attack();
}
return -1; //lose
}
else
{
return 0;
}
}
}
游戏界面

游戏界面也与上次相同:

感悟

本次的作业,主要是对于上一次作业的代码结构进行了调整,使得各部分代码各司其职,分工合作。但在初看这些类关系的时候,确实让我费了一番功夫才看明白。但也真的学习到了不少,让我对于游戏框架设计有了更加深入的认识,特别是回调函数callback的使用,在一些特定的场景十分有效。

遗憾的是本来想对游戏画面进行进一步完善的,但是近期作业较多,没有抽出时间,希望以后可以有机会。

CATALOG
  1. 1. 基本操作演练
  2. 2. 编程实践
    1. 2.1. SSAction
    2. 2.2. SSActionCallback
    3. 2.3. CCMoveToAction
    4. 2.4. CCSequenceAction
    5. 2.5. SSActionManager
    6. 2.6. CCActionManager
    7. 2.7. 裁判类
  3. 3. 游戏界面
  4. 4. 感悟