目录
一、基本操作演练【建议做】
1、下载Fantasy Skybox FREE,构建自己的游戏场景。
1.1 导入Fantasy Skybox FREE资源
在Windows->Asset Store中找到对应的Fantasy Skybox FREE资源,然后下载资源并点击Import导入即可。
导入后,我们可以在Project中的Assets目录下找到对应的文件夹,里面包含此天空盒所对应的材料和场景。
1.2 创建一块用于制作场景的地
选择菜单栏中的GameObject->3D Object->Terrain,建立一块地,然后可以用unity提供的地形设计工具来改变这块地的地形和贴图。
最上面一行是unity提供的简单工具,从左到右依次是:创建相邻地块、绘制地形、种树、种草和设置。
而在绘制地形中又有多种选项,分别是:
升高或降低地形、绘制洞、绘制纹理、设置高度、平滑高度、绘制有特定地质特征的纹理。
这里我没有使用自己绘制的地形(太丑了),将地形绘制工具的各种画笔功能尝试了几遍后,便用天空盒中附带的地形(种了一些树和花草完善原来的地形)来接着后面的操作了。
1.3 创建一个天空盒
选择菜单栏中的Assets->Create->Material,创建一个新的材料,命名为skybox。
然后选择Shader,在下拉栏中找到Skybox选项,将着色器改为天空盒模式,在Skybox中有四种模式,分别是:六面体天空盒、立体天空盒、全景天空盒、纯手写天空盒
这里我们用Cubemap,创建好天空盒材料后,将选定好的图片加进去,便成了我们所要的天空盒了。
1.4 将天空盒加入摄像机组件中
选择摄像机,在右边的Inspector中选择Add Component添加组件,在下拉栏中找到Rendering,里面便有我们需要的skybox组件,添加后,将我们所建立的天空盒加到Custom Skybox中,这样子天空盒便加入到摄像机组件中了。
1.5 运行截图
2、总结
在unity中,有很多种游戏对象,包括空对象、3D游戏对象、2D游戏对象、事件、灯光、声音、视频、UI界面、摄像机等。其中我们最常用的就是3D游戏对象、灯光、声音、UI界面和摄像机。
游戏对象是游戏中人物、道具、场景等对象的总称,它们有基础的属性,但没有具体的独特的功能,用于容纳不同的组件。只有挂载了组件后,一个游戏对象才有了属于它的功能。
二、编程实践
1、目标
- 完成牧师与魔鬼动作分离版。
- 【2019新要求】设计一个裁判类,当游戏达到结束条件时,通知场景控制器结束游戏
2、回顾
在上一节课中,我们也是完成的牧师与魔鬼游戏,当时是利用基础的MVC架构,来设计牧师与魔鬼。下面回顾一下MVC架构:
Model
模型,负责建立起各个游戏对象和它们的基本行为,在牧师与魔鬼中的模型有牧师、魔鬼、船、河以及2个河岸。其中船和河岸被设计为容器,可以承载牧师和魔鬼这两个角色并记录它们的位置;牧师和魔鬼则是作为游戏的主要角色,可以在船和河岸之间来回移动。
View
视图,与用户交互的接口,是我们用户最终可以看到的界面。它能够接收用户的输入信息(比如点击或者键盘按键输入),并根据所点击的游戏对象不同而传递不同的信息,并把一些重要信息反馈给用户。
Controller
控制器,用于将模型和视图连接起来,以达到全局控制的目的。控制器不仅要从模型那里获取相应的信息,根据这些信息判断模型的位置和状态,而且还需要从视图处获得用户的输入信息,进行相应的物体操作。最后控制器还需要判断游戏的进行程度,判断游戏的进度和输赢状态,并将输赢信息返回到视图上,反馈给用户。
3、动作分离思路
从回顾中可以看出,控制器Controller所承担的工作太多了,在设计起来显得很复杂,很容易出现错误。所以动作分离的要求便应运而生。
动作分离,顾名思义,就是将动作从控制器中分离出来,这里需要分离出来的有船的移动和角色(牧师和魔鬼)的移动。
4、动作分离实现
这里的实现很大程度上参照了老师所给的课程文档中的4.2 核心代码与设计解读。
- 首先,我们要定义一个动作的基类,用于存储动作的基本运动:平移、旋转和缩放。
在这个基类中,我们可以没有区分是具体的哪一个动作,也就是说,如果我们直接调用基类,并不会有任何操作,所以这里实现了虚函数,以便后面子类进行重写。
public class SSAction : ScriptableObject {
public bool enable = true;
public bool destroy = false;
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callBack { get; set; }
public virtual void Start () {
throw new System.NotImplementedException ();
}
public virtual void Update () {
throw new System.NotImplementedException ();
}
}
- 接着,我们在动作基类的基础上,实现这一次作业所需要的具体动作。
而在牧师与魔鬼游戏中,船只跟角色都只需要平移即可,不用旋转和缩放,所以我们的具体动作只需要实现平移操作,让物体从一个位置以一定的速度平移到目标位置。
public class SSMoveToAction : SSAction {
public Vector3 target;
public float speed;
private SSMoveToAction () { }
public static SSMoveToAction GetSSAction (Vector3 target, float speed) {
SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction> ();
action.target = target;
action.speed = speed;
return action;
}
public override void Start () {
}
public override void Update () {
this.transform.position = Vector3.MoveTowards (this.transform.position, target, speed * Time.deltaTime);
if (this.transform.position == target) {
this.destroy = true;
this.callBack.SSActionEvent (this);
}
}
}
- 在具体的简单动作基础上,又延伸出了组合动作,让物体通过不同动作的组合来到达终点。
在游戏中,由于我们河岸的位置是比河要高的,也就是说,角色的水平位置比船高,那么如果我们直接采用一段平移过去的话,角色就会穿过河岸陷入地底然后到达船上,这明显是不合理的。所以我们需要将角色从河岸刀船上分解为两段平移:先平移到船的上方,再落下到船上。
public class CCSequenceAction : SSAction, ISSActionCallback {
public List<SSAction> sequence;
public int repeat = -1;
public int start = 0;
public static CCSequenceAction GetSSAcition (int repeat, int start, List<SSAction> sequence) {
CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction> ();
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}
public override void Update () {
if (sequence.Count == 0) return;
if (start < sequence.Count) {
sequence[start].Update ();
}
}
public void SSActionEvent (SSAction action) {
action.destroy = false;
this.start++;
if (this.start >= sequence.Count) {
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0) {
this.destroy = true;
this.callBack.SSActionEvent (this);
}
}
}
public override void Start () {
foreach (SSAction action in sequence) {
action.gameObject = this.gameObject;
action.transform = this.transform;
action.callBack = this;
action.Start ();
}
}
void OnDestroy () {
foreach (SSAction action in sequence) {
Destroy (action);
}
}
}
- 设计好动作后,我们需要能够调用动作,也就是说,我们需要一个类,来管理这些平移操作,让我们所设计好的动作最终能实现为我们所需要的动作。
在这里,我们实现的是一个基类,只是将这些动作构造成一个队列,等待添加河删除,同时对外提供RunAction接口,让外界可以添加动作。
public class SSActionManager : MonoBehaviour {
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction> ();
private List<SSAction> waitingForAdd = new List<SSAction> ();
private List<int> waitingForDelete = new List<int> ();
protected void Update () {
foreach (SSAction action in waitingForAdd) {
actions[action.GetInstanceID ()] = action;
}
waitingForAdd.Clear ();
foreach (KeyValuePair<int, SSAction> pair in actions) {
SSAction action = pair.Value;
if (action.destroy) {
waitingForDelete.Add (action.GetInstanceID ());
} else if (action.enable) {
action.Update ();
}
}
foreach (int key in waitingForDelete) {
SSAction action = actions[key];
actions.Remove (key);
Destroy (action);
}
waitingForDelete.Clear ();
}
public void RunAction (GameObject gameObject, SSAction action, ISSActionCallback callback) {
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callBack = callback;
waitingForAdd.Add (action);
action.Start ();
}
}
- 在动作管理基类的基础上,我们要进一步设计一个子类,以便我们的控制器可以调用,实现船只的移动和角色的移动。
public class ScenceActionManager : SSActionManager, ISSActionCallback {
SSMoveToAction boatAction;
CCSequenceAction characterAction;
Controller controller;
Judger judger;
private void Start () {
controller = SSDirector.getInstance ().currentSceneController as Controller;
controller.actionManager = this;
judger = controller.judger;
}
public void moveBoat (GameObject boat, Vector3 pos, float speed) {
judger.setForbid (true);
boatAction = SSMoveToAction.GetSSAction (pos, speed);
Debug.Log ("Ready to run!");
this.RunAction (boat, boatAction, this);
}
public void moveCharacter (GameObject chr, Vector3 pos, float speed) {
judger.setForbid (true);
Vector3 start = chr.transform.position;
Vector3 tmp = pos;
if (start.y > pos.y) {
tmp.y = start.y;
} else if (start.y < pos.y) {
tmp.x = start.x;
}
SSAction act1 = SSMoveToAction.GetSSAction (tmp, speed);
SSAction act2 = SSMoveToAction.GetSSAction (pos, speed);
characterAction = CCSequenceAction.GetSSAcition (1, 0, new List<SSAction> { act1, act2 });
this.RunAction (chr, characterAction, this);
}
public void SSActionEvent (SSAction action) {
judger.setForbid (false);
}
}
5、裁判类的实现
2019年新加的要求需要我们设计一个裁判类,其实很简单,就是将原来放在控制器中的判断输赢的函数分离出来,单独成为一个类就好了。
public class Judger : MonoBehaviour {
public int checkGame()
{
Controller controller = SSDirector.getInstance().currentSceneController as Controller;
int leftP = 0, rightP = 0, leftD = 0, rightD = 0;
int[] LCount = controller.leftBank.getCount();
leftP += LCount[1];
leftD += LCount[0];
int[] RCount = controller.rightBank.getCount();
rightD += RCount[0];
rightP += RCount[1];
int[] Bcount = controller.boat.getCount();
//Debug.Log(Bcount[0] + " " + Bcount[1]);
if (leftD + leftP == 6) // win
return 2;
// 计算上船上的人
if (controller.boat.getLR() == 0) {
leftP += Bcount[1];
leftD += Bcount[0];
}
else if (controller.boat.getLR() == 1) {
rightP += Bcount[1];
rightD += Bcount[0];
}
Debug.Log("RP: " + rightP + " RD: " + rightD); //测试用
// 如果魔鬼数量大于牧师,并且牧师数量不为0就失败!
if (leftP < leftD && leftP != 0) {
return 0;
}
if (rightP < rightD && rightP != 0) {
return 0;
}
return 1;
}
public void setForbid(bool b) {
Controller controller = SSDirector.getInstance().currentSceneController as Controller;
controller.forbid = b;
}
}
6、挂载运行
创建一个空游戏对象,将设计好的Controller文件挂载上去即可。(注意:这里要先做好船只、河岸、河、牧师和魔鬼的预制)。