Unity3d学习之路-牧师与魔鬼V2(动作分离版)
该版本改进的目的
- 把每个需要移动的游戏对象的移动方法提取出来,建立一个动作管理器来管理不同的移动方法。
- 对于上一个版本,每一个可移动的游戏对象的组件都有一个Move脚本,当游戏对象需要移动时候,游戏对象自己调用Move脚本中的方法让自己移动。而动作分离版,则剥夺了游戏对象自己调用动作的能力,建立一个动作管理器,通过场景控制器(在我的游戏中是Controllor)把需要移动的游戏对象传递给动作管理器,让动作管理器去移动游戏对象。
- 当动作很多或是需要做同样动作的游戏对象很多的时候,使用动作管理器可以让动作很容易管理,也提高了代码复用性。
改进后动作管理部分类图如下(Controllor类只写了新增的函数和属性)
动作管理类图描述与实现
- ISSActionCallback(动作事件接口)
作为动作和动作管理者的接口(组合动作也可以是动作管理者),动作管理者继承这个接口,并且实现接口的方法。当动作完成的时候,动作会调用这个接口,发送消息告诉动作管理者对象,这个动作做完啦,然后管理者会对下一个动作进行处理。
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);
}
- SSActionManager(动作管理基类)
管理SequenceAction和SSAction,可以给它们传递游戏对象,让游戏对象做动作或是一连串的动作,控制动作的切换。SSActionManager继承了ISSActionCallback接口,通过这个接口,当动作做完或是连续的动作做完时候会告诉SSActionManager,然后SSActionManager去决定如何执行下一个动作。
public class SSActionManager: MonoBehaviour, ISSActionCallback
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //将执行的动作的字典集合,int为key,SSAction为value
private List<SSAction> waitingAdd = new List<SSAction>(); //等待去执行的动作列表
private List<int> waitingDelete = new List<int>(); //等待删除的动作的key
protected void Update()
{
foreach(SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac; //获取动作实例的ID作为key
}
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);
DestroyObject(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();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
//牧师与魔鬼的游戏对象移动完成后就没有下一个要做的动作了,所以回调函数为空
}
}
- SequenceAction(组合动作实现)
SequenceAction继承了ISSActionCallback,因为组合动作是每一个动作的顺序完成,它管理这一连串动作中的每一个小的动作,所以当小的动作完成的时候,也要发消息告诉它,然后它得到消息后去处理下一个动作。SequenceAction也继承了SSAction,因为成个组合动作也需要游戏对象,也需要标识是否摧毁,也会有一个组合动作的管理者的接口,组成动作也是动作的子类,只不过是让具体的动作组合起来做罢了。
public class SequenceAction: SSAction, ISSActionCallback
{
public List<SSAction> sequence; //动作的列表
public int repeat = -1; //-1就是无限循环做组合中的动作
public int start = 0; //当前做的动作的索引
public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)
{
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();//让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()
{
//如果组合动作做完第一个动作突然不要它继续做了,那么后面的具体的动作需要被释放
}
}
- SSAction (动作基类)
SSAction是所有动作的基类。SSAction继承了ScriptableObject代表SSAction不需要绑定GameObject对象,且受Unity引擎场景管理。
public class SSAction : ScriptableObject //动作
{
public bool enable = true; //是否正在进行此动作
public bool destroy = false; //是否需要被销毁
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //动作完成后的消息通知者
protected SSAction() {} //保证SSAction不会被new
//子类可以使用下面这两个函数
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
牧师与魔鬼动作管理实现
- SSMoveToAction(移动动作实现)
以speed的速度向target目的地移动
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>();//让unity自己创建一个MoveToAction实例,并自己回收
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)
{
this.destroy = true;
this.callback.SSActionEvent(this); //告诉动作管理或动作组合这个动作已完成
}
}
public override void Start()
{
//移动动作建立时候不做任何事情
}
}
- MySceneActionManager(移动动作管理实现)
船的移动是一个SSMoveToAction动作就可以,而角色的移动需要两个SSMoveToAction动作组合(先垂直后水平移动或先水平后垂直移动)。然后设置当前场景控制器的动作管理者为MySceneActionManager,这样场景控制器就可以调用动作管理器的方法实现不同游戏对象的移动啦。
public class MySceneActionManager:SSActionManager
{
public SSMoveToAction moveBoatToEndOrStart; //移动船到结束岸,移动船到开始岸
public SequenceAction moveRoleToLandorBoat; //移动角色到陆地,移动角色到船上
public Controllor sceneController; //当前场景的场景控制器
protected new void Start()
{
sceneController = (Controllor)SSDirector.GetInstance().CurrentScenceController;
sceneController.actionManager = this; //设置该场景控制器的动作管理者为自己
}
public void moveBoat(GameObject boat, Vector3 target, float speed)
{
moveBoatToEndOrStart = SSMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, moveBoatToEndOrStart, this);
}
public void moveRole(GameObject role, Vector3 middle_pos, Vector3 end_pos,float speed)
{
SSAction action1 = SSMoveToAction.GetSSAction(middle_pos, speed);
SSAction action2 = SSMoveToAction.GetSSAction(end_pos, speed);
moveRoleToLandorBoat = SequenceAction.GetSSAcition(1, 0, new List<SSAction>{action1, action2}); //1是做一次动作组合,0代表从action1开始
this.RunAction(role, moveRoleToLandorBoat, this);
}
}
- Controllor(当前场景控制器)
最后Controllor接收到游戏对象需要移动的时候,只要传给MySceneActionManager相应的参数和游戏对象就可以进行移动啦,修改了的部分代码如下:
SSActionManager继承了MonoBehaviour。MonoBehaviour是每个脚本的基类,又继承于Behaviours。只有继承了MonoBehaviour的类才可以作为组件挂载到游戏对象上,在unity 中所有继承MonoBehaviour的类是不可以实例化的,因为unity都会自动为其创建实例,所以我们需要调用该类的时候不使用new,而是使用AddComponent来调用。
public MySceneActionManager actionManager;
void Start ()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
LoadResources();
actionManager = gameObject.AddComponent<MySceneActionManager>() as MySceneActionManager;
}
public void MoveBoat() //移动船
{
if (boat.IsEmpty() || user_gui.sign != 0) return;
actionManager.moveBoat(boat.getGameObject(),boat.BoatMoveToPosition(),boat.move_speed); //动作管理器实现移动船
user_gui.sign = Check();
if (user_gui.sign == 1)
{
for (int i = 0; i < 3; i++)
{
roles[i].PlayGameOver();
roles[i + 3].PlayGameOver();
}
}
}
public void MoveRole(RoleModel role) //移动角色
{
if (user_gui.sign != 0) return;
if (role.IsOnBoat())
{
LandModel land;
if (boat.GetBoatSign() == -1)
land = end_land;
else
land = start_land;
boat.DeleteRoleByName(role.GetName());
//动作分离版本改变
Vector3 end_pos = land.GetEmptyPosition();
Vector3 middle_pos = new Vector3(role.getGameObject().transform.position.x, end_pos.y, end_pos.z);
actionManager.moveRole(role.getGameObject(), middle_pos, end_pos, role.move_speed);
role.GoLand(land);
land.AddRole(role);
}
else
{
LandModel land = role.GetLandModel();
if (boat.GetEmptyNumber() == -1 || land.GetLandSign() != boat.GetBoatSign()) return; //船没有空位,也不是船停靠的陆地,就不上船
land.DeleteRoleByName(role.GetName());
//动作分离版本改变
Vector3 end_pos = boat.GetEmptyPosition();
Vector3 middle_pos = new Vector3(end_pos.x, role.getGameObject().transform.position.y, end_pos.z);
actionManager.moveRole(role.getGameObject(), middle_pos, end_pos, role.move_speed);
role.GoBoat(boat);
boat.AddRole(role);
}
user_gui.sign = Check();
if (user_gui.sign == 1)
{
for (int i = 0; i < 3; i++)
{
roles[i].PlayGameOver();
roles[i + 3].PlayGameOver();
}
}
}
skybox
天空盒是围绕整个场景的包装器,其实就是一个六面体盒子,这个盒子的六面是不同的Textrue,这样就生成了类似真实世界的天空,天空盒里面装的是你的游戏世界,这样用相机观察世界的时候就可以看到很好看的天空背景。官方描述:点击这里
- 制作
首先新建一个Material,在Shader那里选择Skybox/6 Sided(版本2017.4),然后在每个Textrue那里加入上下左右前后的图片,这样天空盒就做好啦 - 挂载
点击主相机,添加组件Skybox,然后把Material拖进去,在游戏场景里就可以看到制作好的天空了
游戏实现截图
小结
动作分离之后,增加了动作基类,组合动作,具体动作,动作事件接口,动作管理基类,具体动作管理。就从牧师与魔鬼这个游戏来看,整个游戏只有移动这个动作和折线移动这个组合动作,所以像动作事件接口就显得没有什么作用,具体的动作管理似乎也显得有些累赘。但是这个架构,当游戏动作增加,游戏复杂度增加的时候,整个结构可以方便扩展和管理,避免了控制类,动作类,游戏模型之间的直接依赖。
unity初学阶段,可能很多理解不到位或是错误的,欢迎指出和建议。
牧师与魔鬼V2(动作分离版)代码:传送门