作业内容
本文主要针对unity的游戏对象的使用做出总结,以及介绍天空盒的使用,并在前一周项目(已实现MVC模式的牧师与恶魔)的基础上进行改进,完成牧师与恶魔的动作分离版本。下文将介绍如何利用动作管理器来进行修改。
操作与总结
1.参考 Fantasy Skybox FREE 构建自己的游戏场景
- 在主摄像机加入Skybox组件后,拖动已经实现的天空材料,由此生成天空盒。
- 加入一个Plane与一个Cube游戏对象。
- 增加多一个摄像头,实现Cube的俯视图,需要注意的是调整摄像头的Depth大于-1,且将视图的大小和位置调整至右下角。
实现效果如下图所示:
2.写一个简单的总结,总结游戏对象的使用
- 具体上来说,所有游戏对象都有Active属性,Name属性,Tag属性等。每个游戏对象 (GameObject) 还包含一个变换transform组件。我们可以通过这个组件来使游戏对象改变位置,旋转和缩放。我们还可以添加许许多多不同的组件或脚本来增加游戏对象的功能。
细节上来说,主要包括以下几大类
- 常见游戏对象
如正方体,球体,平面等等,还有空对象,不显示却是最常用的对象之一。 - Camera 摄像机
它是观察游戏世界的窗口,Projection属性包括正交视图与透视视图。Viewport Rect:属性是控制摄像机呈现结果对应到屏幕上的位置以及大小。屏幕坐标系:左下角是(0, 0),右上角是(1, 1)。Depth属性是当多个摄像机同时存在时,这个参数决定了呈现的先后顺序,值越大越靠后呈现。 - skyboxes 天空盒
天空是一个球状贴图,通常用 6 面贴图表示。
使用方法有两步,第一为在摄像机添加天空组件。Component 加入skybox组件,第二为直接拖入天空材料(Material)。 - 光源Light
灯光类型(type)包括平行光(类似太阳光),聚光灯(spot),点光源(point),区域光(area,仅烘培用)。 地形构造工具Terrain
属性解释如图所示:
声音audio
将声音素材拖入摄像机就可以成为背景音乐。可以设置是否重复,音量等属性- 游戏资源库
从商店中查找所需资源后,import packages后就可以在resource中自由使用这些资源。
- 常见游戏对象
编程实践
牧师与魔鬼 动作分离版
上周完成的牧师与魔鬼游戏利用了MVC模式,而其中的动作由一个为moveable类来控制。这种结构显然并不利于我们对于游戏日后的扩展和更改,所以本周,我们学习了建立动作管理器,将动作分离出来。我们可以建立一个动作管理器,通过场景控制器把需要移动的游戏对象、移动目标和速度等传递给动作管理器,让动作管理器去移动游戏对象,实现该动作。
UML图如下:
1.首先,SSAction是一个动作基类,里面使用virtual申明虚方法,通过重写实现多态。这样继承者就明确使用Start和Update编程游戏对象行为。它利用接口实现消息通知,避免与动作管理者直接依赖。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//ScriptableObject 是不需要绑定 GameObject对象的可编程基类。
//这些对象受Unity引擎场景管理
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() { }
// Use this for initialization
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
2.接着,通过继承SSAction这个虚基类来实现单个动作的完成CCMoveToAction类以及多个动作连续完成CCSequenceAction类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//仅仅执行单个移动函数,通过GetSSAction来得到目的地和速度
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;
}
// 多态
public override void Start(){}
// 多态
public override void Update()
{
this.transform.position = Vector3.MoveTowards(this.transform.position, target, 10f * Time.deltaTime);
if (this.transform.position == target)
{
//waiting for destroy;
this.destory = true;
this.callback.SSActionEvent(this);
}
}
}
这是标准的组合设计模式。被组合的对象和组合对象属于同一种类型。通过组合模式,我们能实现几乎满足所有越位需要、非常复杂的动作管理。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//仅仅执行多个移动函数,将动作存在队列中。通过GetSSAction来得到目的地和速度
public class CCSequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1; //repeat forever
public int start = 0;
public static CCSequenceAction GetSSAction(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 Start()
{
foreach (SSAction action in sequence)
{
action.gameobject = this.gameobject;
action.transform = this.transform;
action.callback = this;
action.Start();
}
}
// 执行当前动作
public override void Update()
{
if (sequence.Count == 0) return;
if (start < sequence.Count)
sequence[start].Update();
}
// 收到当前动作执行完成,推下一个动作,如果完成一次循环,减次数。如完成,通知该动作的管理者。
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
source.destory = false;
this.start++;
if (this.start >= sequence.Count)
{
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0)
{
this.destory = true;
this.callback.SSActionEvent(this);
}
else
{
sequence[start].Start();
}
}
}
private void OnDestroy()
{
//destory
}
}
3.动作事件接口(ISSActionCallback接口),这个接口是动作与动作管理者之间交流的方式,动作管理者继承并实现接口。当动作完成时,动作通过该接口通知动作管理者对象,这个动作已完成,然后管理者可以处理下一个动作。如此往复,直到所有动作都执行完成。
- 事件类型定义,使用了枚举变量
- 定义了事件处理接口,所有事件管理者都必须实现这个接口,来实现事件调度。所以,组合事件需要实现它,事件管理器也必须实现它。
- 这里还展示了语言函数默认参数的写法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType : int { Started, Competeted }
public interface ISSActionCallback
{
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null);
}
4.然后实现动作管理基类SSActionManager类 ,它来管理不同的动作,将动作放在链表中等待执行或等待删除。还有能从ISSActionCallback接口中得知动作是否完成,是否需要销毁。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//创建 MonoBehaiviour 管理一个动作集合,动作做完自动回收动作。
public class SSActionManager : MonoBehaviour,ISSActionCallback
{
public Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
// 开始动作
protected void Start()
{
}
// 该类演示了复杂集合对象的使用。
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);
DestroyObject(ac);
}
waitingDelete.Clear();
}
// 提供了运行一个新动作的方法。该方法把游戏对象与动作绑定,并绑定该动作事件的消息接收者。
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
{
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
action.destory = false;
action.enable = true;
waitingAdd.Add(action);
action.Start();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
}
}
5.继承SSActionManager基类实现动作管理者CCActionManager。场景控制器就可以调用该类的方法实现不同游戏对象的移动,如通过moveBoatAction来实现船的移动,仅仅需要单个动作,利用CCAction类;而通过moveCharacterAction实现角色的移动,需要两个动作,所以需要CCSequenceAction类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Mygame;
//这是实战的管理器。所以它需要与场景控制器配合。
public class CCActionManager : SSActionManager
{
public CCMoveToAction ccmoveBoat; //移动船
public CCSequenceAction ccmoveCharacter; //移动角色
public FirstController sceneController;
protected new void Start()
{
sceneController = (FirstController)Director.getInstance().sceneController;
sceneController.actionManager = this;
}
public void moveBoatAction(GameObject boat, Vector3 target, float speed)
{
ccmoveBoat = CCMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, ccmoveBoat, this);
}
public void moveCharacterAction(GameObject character, Vector3 middle_pos, Vector3 end_pos, float speed)
{
SSAction action1 = CCMoveToAction.GetSSAction(middle_pos, speed);
SSAction action2 = CCMoveToAction.GetSSAction(end_pos, speed);
ccmoveCharacter = CCSequenceAction.GetSSAction(1, 0, new List<SSAction> { action1, action2 }); //1表示做一次动作,0表示从初始action1开始
this.RunAction(character, ccmoveCharacter, this);
}
}
6.最后,我们只需要改动上一周所写好的FirstController类调用动作管理器的函数实现动作,而不是利用moveable类中的函数。以下是改动过后的FirstController类,改动部分都已经标记。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Mygame;
public class FirstController : MonoBehaviour, SceneController, UserAction {
Vector3 water_pos = new Vector3(0, 0.5f, 0);
public CoastController leftCoast;
public CoastController rightCoast;
public BoatController boat;
private MyCharacterController[] characters = null;
private UserGUI userGUI = null;
public bool flag_stop = false;
Vector3 target;
public float speed = 2.0f;
public CCActionManager actionManager;
//得到唯一的实例
void Awake()
{
Director director = Director.getInstance();
director.sceneController = this;
userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
characters = new MyCharacterController[6];
load();
flag_stop = false;
}
//初始化游戏资源,如角色,船等等
public void load()
{
GameObject water = Instantiate(Resources.Load("Perfabs/water", typeof(GameObject)), water_pos, Quaternion.identity, null) as GameObject;
water.name = "water";
leftCoast = new CoastController("left");
rightCoast = new CoastController("right");
boat = new BoatController();
for (int i = 0; i < 3; i++)
{
MyCharacterController character = new MyCharacterController("priest");
character.setPosition(leftCoast.getEmptyPosition());
character.Oncoast(leftCoast);
leftCoast.getOnCoast(character);
characters[i] = character;
character.setName("priest" + i);
}
for (int i = 0; i < 3; i++)
{
MyCharacterController character = new MyCharacterController("devil");
character.setPosition(leftCoast.getEmptyPosition());
character.Oncoast(leftCoast);
leftCoast.getOnCoast(character);
characters[i + 3] = character;
character.setName("devil" + i);
}
}
//判断游戏胜负
int check_game_over()
{
int left_priest = 0, left_devil = 0, right_priest = 0, right_devil = 0;
int[] fromCount = leftCoast.getCharacterNum();
int[] toCount = rightCoast.getCharacterNum();
left_priest += fromCount[0];
left_devil += fromCount[1];
right_priest += toCount[0];
right_devil += toCount[1];
//获胜条件
if (right_priest + right_devil == 6)
return 1;
int[] boatCount = boat.getCharacterNum();
//统计左右两岸的牧师与恶魔的数量
if (!boat.get_is_left())
{
right_priest += boatCount[0];
right_devil += boatCount[1];
}
else
{
left_priest += boatCount[0];
left_devil += boatCount[1];
}
//游戏失败条件
if ((left_priest < left_devil && left_priest > 0)|| (right_priest < right_devil && right_priest > 0))
{
return -1;
}
return 0; //游戏继续
}
//重置函数
public void restart()
{
boat.reset();
leftCoast.reset();
rightCoast.reset();
for (int i = 0; i < characters.Length; i++)
{
characters[i].reset();
}
}
//游戏结束后,不能再点击产生交互信息
public bool stop()
{
if(check_game_over() != 0)
return true;
return false;
}
//动作
public void moveBoat() //移动船
{
if (boat.isEmpty()) return;
//改动
actionManager.moveBoatAction(boat.getBoat(), boat.BoatMoveToPosition(), speed); //调用动作管理器中的移动船函数,原先为调用moveable函数
userGUI.status = check_game_over();
}
public void characterIsClicked(MyCharacterController character) //移动角色
{
if (userGUI.status != 0) return;
if (characater.getis_onbot())
{
CoastController coast;
if (!boat.get_is_left())
{
coast = rightCoast;
}
else
{
coast = leftCoast;
}
boat.GetOffBoat(character.getName());
//改动
Vector3 end_pos = coast.getEmptyPosition();
Vector3 middle_pos = new Vector3(character.getGameObject().transform.position.x, end_pos.y, end_pos.z);
actionManager.moveCharacterAction(character.getGameObject(), middle_pos, end_pos, speed); //调用动作管理器中的移动角色函数,原先为调用moveable函数
character.Oncoast(coast);
coast.getOnCoast(character);
}
else
{
CoastController coast = character.getcoastController();
// 船上已有两人
if (boat.getEmptyIndex() == -1)
{
return;
}
// 船与角色并不在同一边岸
if (coast.get_is_right() == boat.get_is_left())
return;
coast.getOffCoast(character.getName());
//改动
Vector3 end_pos = boat.getEmptyPos();
Vector3 middle_pos = new Vector3(end_pos.x, character.getGameObject().transform.position.y, end_pos.z);
actionManager.moveCharacterAction(character.getGameObject(), middle_pos, end_pos, character.speed); //调用动作管理器中的移动角色函数,原先为调用moveable函数
character.Onboat(boat);
boat.GetOnBoat(character);
}
userGUI.status = check_game_over();
}
}
7.除此之外,我还利用这节课所学的天空盒的内容,来给我的主摄像机加上的一个skybox属性。而在上周,我是利用一个cube遮挡摄像头的后面来实现bcakground的。改动过后,游戏界面也比上一周变得更加美观。
总结
动作管理者适用于当动作很多或者有很多需要做同样动作的游戏对象的情况,它能够方便地管理所有动作,也提高了代码复用性。我们通过理解UML图可以清楚地知道每个类之间的关系,哪个类是基类,哪个是接口。这样子写代码也就事半功倍了。
到此,这个动作分离+MVC模式的牧师与恶魔就已经完成了。如果想查看两周代码对比的,可以点击下面的传送门。
第一周MVC模式牧师与恶魔
第二周MVC模式+动作管理模式牧师与恶魔
结束语
由于作者水平有限,如有任何错误请指出并讨论,十分感谢!
想了解更多关于3d游戏设计代码,可以点击我的Github一起学习。
Github地址:https://github.com/dick20/3d-learning