游戏对象与图形基础
PS:所有代码请点击下方代码传送门
代码传送门
目录
一、基本操作演练
1. 下载 Fantasy Skybox FREE, 构建自己的游戏场景
话不多说,咱先上个giao图:
要完成上面这个有点捞的效果,很简单,就以下两步:
- 学习使用天空盒
- 学习使用地形编辑
很简单吧,然而还是花了我几个小时(;´д`)ゞ
使用天空盒
关于 skybox 的使用,我找到的有两种途径,只不过有所区别:
- 在 camera 上添加 skybox 组件,然后贴上贴图
- 在 Window->Rendering->Lighting->Environment 窗口的 Skybox Material 上挂载贴图
具体位置如下:
你们肯定会问,这两种有什么区别呢?很简单:
- 在 camera 中设置:仅仅类似于给 camera 的镜头上加了个贴图,只有这个 camera 的预览中会显示出 skybox,实际背景并没有改变
- 在 Lighting->Environment 中设置则是直接贴在了背景上,所以无论哪个相机的预览都能看到
额,有点抽象,那上图呗(第一张是在 camera,第二张是 Environment):
这下就简单明了了吧
使用地形编辑
不得不说,这玩意儿的使用困扰了我太久了(emmmmm·········
要使用地形编辑,最先要弄的,就是先 右键->3D Object->Terrain,然后就可以开搞了。先介绍下功能咯:
- 地形编辑工具
- 画树
- 画细节
在此之前,我们先通过 Window->Assets Store->Search Online 打开库,然后搜索 Fantasy Skybox Free,加到自己的 unity 中,然后通过 unity 打开。最后就是 download + import 喽。
言归正传,上述的地形编辑功能看上去很简单吧。但是,这里面可真的太多坑了。下面说下我踩的一些坑:
首先吧,地形工具别真的以为它只能用来画个山(加减高度)!!!在其下 Raise or Lower Terrain 选项中,还有如下这些选项:
我在这里真的是卡了一个多小时,不知道怎么画马路。。。其实只需要切到 Paint Texture 后,将 fantasy skybox 中的 texture 加入到 Paint Texture 下的 Terrain Layers 中,然后就可以开始画地表,也就是可以修路了。。。为啥说我卡了很久呢?就是因为我一开始不知道在这修路呀!!!!结果在添加细节那找了半天,晕了
其余功能的使用就各位多点点试试就清楚了,反正我没踩到啥巨坑。
哦对了,还有一个要注意下的,在画细节这一步,加入了细节之后还不行,还得选择好某个添加的细节(就点一下),之后才能画。
最后就是些苦力活咯,多画画,修改啥的,建议调一下光的颜色,黄一点,不然一开始白色的话,那草的颜色,不忍直视。
2. 写一个简单的总结,总结游戏对象的使用
emmmmm,说实话,游戏对象这东西,其实就是得多用多做,做多了就基本记得有啥用了。当然,就现在我这水平,顶多就用点基本的 3d Object 咯,别的就是得灵活用下 Light、Audio、Terrain 等作用非常大的游戏对象。最后,必须一提的就是,学会用 空对象、空对象、空对象!!!这东西,不得不说,虽然一开始觉得脚本必须挂在在一个物体上才能使用有点笨笨的,但主要多用用空对象,用习惯了,就会觉得其实这设计还挺合理的。
二、编程实践
牧师与魔鬼 动作分离版,并设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
开篇说明
首先,特别鸣谢:
- Unity3d学习之路-牧师与魔鬼V2(动作分离版)。传送门
同时,也非常感谢老师的 博客,但具体链接可能不太应该公开?所以就8能放上来了。这次我能比较好的理解整个动作类与类之间关系的原因,就是看了老师博客中的代码,然后上述的 Unity3d学习之路-牧师与魔鬼V2(动作分离版) 的代码中 Actions 也差不多,因此仔细阅读还是能够理解的。
动作分离
在谈如何实现之前,我想先说明下我对于动作分离的理解。简而言之,就是加入一个动作的管理者基类来管理所有动作的执行顺序,而我在使用时,只需要实现一个继承该基类的子类作为对应的具体管理者,具体管理者则提供 场景控制器 一些接口来完成场景控制器所需要的一些动作。
有了这么一个供给链条,我们就可以方便地提供到达目的位置所需的中间位置以及需要移动的对象,就能完成让对象到达目的位置的组合动作,使得代码的可重用性就高很多了,不必要再针对多种动作来复杂地实现对应组合动作。
嗐,表达不太行,莫得啥办法,咱接下来看咯。
具体实现
回调函数接口:ISSActionCallback
这个接口的功能,就是回调,使得当某个单一动作完成时,可以被上层管理者所知道。具体回调的实现逻辑如下(这很重要,一定要理解才能弄懂这个动作分离的实现):
- 单一动作类 CCMoveToAction 继承了 SSAction(注意单一动作与动作基类的区别)
- 当单一动作类所定义的某个直线运动完成后,就会调用回调函数,让上层管理者知道
- 单一动作类的上层管理者有两个:
- 组合动作类 CCSequenceAction
- 具体动作管理者 CCActionManager
- CCSequenceAction 得知 CCMoveToAction 完成动作后,就会去执行组合动作中的下一个动作
- CCActionManager 得知 CCMoveToAction 完成动作后,就会去执行整个动作列表中的下一个动作。注意,这里的下一个动作与上述 CCSequenceAction 中的不同之处是:
- CCSequenceAction 中的是指同一个对象的一个组合动作中的一个部分
- CCActionManager 中的是指完全不同的另一个动作,这个动作可能是另一个游戏对象的一个动作,也可能是同一个对象的第二个或第二组动作
- 因此,CCSequenceAction 以及 CCActionManager 需要实现这个接口,来提供给单一动作类调用,达到传递消息的目的
除此之外,组合动作也会在完成时调用该接口,让上层管理者知道,同样的,组合动作的上层管理者也有组合动作 CCSequenceAction 以及具体动作管理者 CCActionManager。为什么上层还能是组合动作?
// 事件类型定义
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);
}
动作基类:SSAction
这个类呢,怎么说,就是一个动作的共性集合。下面的 Start() 与 Update() 都需要子类去实现。除此之外,有个 非常重要、非常重要、非常重要 的变量:callback。
public class SSAction : ScriptableObject{ //动作
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() { } // 防止用户自己new抽象的対象
public virtual void Start(){ // 申明虚方法,通过重写实现多态
throw new System.NotImplementedException();
}
public virtual void Update(){
throw new System.NotImplementedException();
}
}
单一动作类:CCMoveToAction
该类继承了 SSAction,因此会有一个 ISSActionCallback 接口 callback,在 Update 时,若发现该单一动作已完成,则调用该接口中的函数 SSActionEvent,来让上层接口知道。
public class CCMoveToAction : SSAction{
public Vector3 target;
public float speed;
private CCMoveToAction() { }
public static CCMoveToAction GetSSAction(Vector3 target, float speed){
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();//让unity创建动作类,确保内存正确回收
action.target = target;
action.speed = speed;
return action;
}
public override void Update(){
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
if (this.transform.position == target){
// waiting for destroy
this.destroy = true;
this.callback.SSActionEvent(this); //告诉动作管理或动作组合这个动作已完成
}
}
public override void Start(){
// 不做任何事情
}
}
组合动作类:CCSequenceAction
这个类,有一个非常重要的点,就是在 Start() 函数中,将所有动作类的实例所构成的链表 sequence(可以是单一动作,也可以是另一个组合动作)中的每一个实例的 callback 都设置为自身的 callback,然后再实现对应的 SSActionEvent() 函数。这么做的目的,就是让组合动作中的每一部分在完成时所调用的 callback.SSActionEvent() 就是该组合动作所实现的 SSActionEvent(),从而让组合动作知道要进行下一部分的动作。
// 让动作组合继承抽象动作,能够被进一步组合;实现回调接受,能接收被组合动作的事件
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>();//让unity自己创建一个SequenceAction实例
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(); //一个组合中的一个动作执行完后会调用接口,所以这里看似没有start++实则是在回调接口函数中实现
}
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null){
source.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(){
}
}
动作管理基类:SSActionManager
这个类的实现中,比较重要的是 RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)。这个函数将 动作字典 actions 中的所有动作的 callback 设置成了 RunAction() 中的参数 manager 所表示的接口。这样,具体动作类就可以将其实现的接口作为 RunAction() 参数输入,于是每个通过 RunAction() 加入的动作 SSAction action 的 callback 就是具体动作类的 callback,然后通知时上层就是该具体动作类。
// 创建 MonoBehaiviour 管理一个动作集合,动作做完自动回收动作
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>(); // 待删除动作列表
// 演示了添加、删除等复杂集合对象的使用
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.destroy){
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable){
ac.Update();
}
}
foreach (int key in waitingDelete){
SSAction ac = actions[key];
actions.Remove(key);
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();
}
// Use this for initialization
protected void Start(){
}
}
具体动作类:CCActionManager
这个是我对于本次游戏所设计的具体动作管理者,主要有两个函数:moveBoat() 和 moveRole,作用就是根据需求移动 Boat 或 Role。除此之外,在这个函数中我也通过简单地设置了一个变量 bool someoneIsMoving 来实现当一个动作还没完成时不能加入另一个动作,进而达到禁用click的效果。当一个动作完成时,由于会调用 SSActionEvent(),因此 someoneIsMoving 又会被设置回 false,然后就可以执行另一个动作了。
public class CCActionManager : SSActionManager, ISSActionCallback{
private CCMoveToAction moveBoatToAnotherSide;
private CCSequenceAction moveRoleToShoreOrBoat;
public FirstController sceneController;
private bool someoneIsMoving = false;
protected new void Start(){
sceneController = (FirstController)SSDirector.GetInstance().CurrentScenceController;
sceneController.actionManager = this;
}
public bool IsSomeoneIsMoving(){
return someoneIsMoving;
}
public void moveBoat(BoatModel boat, Vector3 target, float speed){
if(someoneIsMoving)
return;
someoneIsMoving = true;
moveBoatToAnotherSide = CCMoveToAction.GetSSAction(target, speed);
this.RunAction(boat.GetBoat(), moveBoatToAnotherSide, this);
}
public void moveRole(RoleModel role, Vector3 des, float speed){
if(someoneIsMoving)
return;
someoneIsMoving = true;
if(role.OnBoat()){
SSAction mid = CCMoveToAction.GetSSAction(new Vector3(role.GetRole().transform.position.x,des.y,role.GetRole().transform.position.z),speed);
SSAction end = CCMoveToAction.GetSSAction(des,speed);
moveRoleToShoreOrBoat = CCSequenceAction.GetSSAction(1,0,new List<SSAction>{mid,end});
}
else{
SSAction mid = CCMoveToAction.GetSSAction(new Vector3(des.x,role.GetRole().transform.position.y,des.z),speed);
SSAction end = CCMoveToAction.GetSSAction(des,speed);
moveRoleToShoreOrBoat = CCSequenceAction.GetSSAction(1,0,new List<SSAction>{mid,end});
}
this.RunAction(role.GetRole(), moveRoleToShoreOrBoat, this);
}
#region ISSActionCallback implementation
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null){
someoneIsMoving = false;
return;
}
#endregion
}