在我的學習Unity(5)和學習Unity(6)中邓嘹,我們已經(jīng)完成了一個簡單的牧師與惡魔游戲并改進了它的場景。在這一篇文章中汞舱,我想從代碼的架構(gòu)來改進它——實現(xiàn)一個動作管理器來管理場景中的動作夕凝。
美化場景谈宛、控制攝像機和光源這些技巧,可能只適用于游戲編程情妖,甚至只限于Unity3D睬关,但是掌握代碼組織架構(gòu)和面向?qū)ο蟮乃枷?/strong>對任何方面的編程都有巨大的提升。
下載我的項目在本地查看毡证!
從我的github下載項目資源电爹,將Assets文件夾覆蓋你的項目中的Assets文件夾,然后在U3D中雙擊“ass”料睛,就可以運行了丐箩!
為什么要引入動作管理器
在一個場景中肯定有很多“會動”的物體摇邦,它們的運動是有很多共性的,如果我們?yōu)橛螒蚪巧珜崿F(xiàn)一個運動方法雏蛮,為船實現(xiàn)一個運動方法涎嚼,為將來出現(xiàn)的所有會動的物體都實現(xiàn)一個運動方法,勢必是一種資源的浪費挑秉。我們可以將運動的共性提取出來法梯,用一個管理器統(tǒng)一管理,這樣犀概,代碼的復用性和可讀性都會提高立哑。
什么是動作管理器
- 動作管理器就是一個對象,管理整個場景中所有的動作姻灶。
- 一個SceneController(場景管理器)只配備一個動作管理器對象铛绰。
- 不管是游戲角色的移動還是船的移動,都歸這個對象管产喉;
- 動作管理器可以添加動作(添加的時候要指定動作所作用的GameObject)捂掰,監(jiān)測已經(jīng)完成的動作并清除。
我下面對重要的類做出解釋曾沈。
ActionCallback
這個接口很簡單这嚣,就一個方法。實現(xiàn)了這個接口的類塞俱,就可以知道到“某個動作已完成”(動作一完成actionDone方法就會被調(diào)用)姐帚,并對這個事件做出反應。
public interface ActionCallback {
void actionDone(ObjAction source);
}
ObjAction
ObjAction是所有動作的基類障涯。ActionManager就是通過ObjAction這個接口來管理動作的罐旗。
public class ObjAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject;
public Transform transform;
public ActionCallback whoToNotify;
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
ObjAction保存了這個動作作用的對象和這個對象的Transform組件(這里有一點多余了)。ObjAction只是一個動作的抽象唯蝶,具體如何實現(xiàn)動作要讓它的子類來實現(xiàn)Update方法九秀。注意ObjAction并不是MonoBehaviour的子類,它的Start和Update方法不會主動調(diào)用粘我,我們會在后面一個MonoBehaviour類的Update中調(diào)用這個對象的Update方法鼓蜒。
這個類還定義了一個ActionCallback的成員,用來保存動作完成時要通知的對象涂滴。
為什么要繼承ScriptableObject呢?因為ScriptableObject有一些生命周期方法晴音,等一下它的子類就要用到OnDestroy柔纵。
MoveToAction
MoveToAction是ObjAction的一個實現(xiàn),它代表一個直線移動的動作锤躁。
public class MoveToAction : ObjAction
{
public Vector3 target;
public float speed;
private MoveToAction(){}
public static MoveToAction getAction(Vector3 target, float speed) {
MoveToAction action = ScriptableObject.CreateInstance<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.whoToNotify.actionDone(this);
}
}
public override void Start() {
//
}
}
MoveToAction不能直接通過new來得到對象搁料,只能通過它的靜態(tài)方法getAction()來新建實例。
當MoveToAction發(fā)現(xiàn)自己的動作完成的時候,它會將自己標識為“要被銷毀”郭计,并通過this.whoToNotify.actionDone(this);
告知whoToNotify動作已完成霸琴。
SequenceAction
SequenceAction是ObjAction的另一個子類,它代表一連串MoveToAction組成的動作昭伸,也就是折線移動梧乘。
public class SequenceAction: ObjAction, ActionCallback {
public List<ObjAction> sequence;
public int repeat = 1; // 1->only do it for once, -1->repeat forever
public int currentActionIndex = 0;
public static SequenceAction getAction(int repeat, int currentActionIndex, List<ObjAction> sequence) {
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.sequence = sequence;
action.repeat = repeat;
action.currentActionIndex = currentActionIndex;
return action;
}
public override void Update() {
if (sequence.Count == 0)return;
if (currentActionIndex < sequence.Count) {
sequence[currentActionIndex].Update();
}
}
public void actionDone(ObjAction source) {
source.destroy = false;
this.currentActionIndex++;
if (this.currentActionIndex >= sequence.Count) {
this.currentActionIndex = 0;
if (repeat > 0) repeat--;
if (repeat == 0) {
this.destroy = true;
this.whoToNotify.actionDone(this);
}
}
}
public override void Start() {
foreach(ObjAction action in sequence) {
action.gameObject = this.gameObject;
action.transform = this.transform;
action.whoToNotify = this;
action.Start();
}
}
void OnDestroy() {
foreach(ObjAction action in sequence) {
DestroyObject(action);
}
}
}
按照sequence的動作順序,一個一個地執(zhí)行下來庐杨,如果repeat大于0則從頭再執(zhí)行一次选调。
注意actionDone中有一句話source.destroy = false;
因為MoveToAction到達指定地點以后會自動將自己標識為destroy,這一句話阻止它被銷毀灵份,因為如果有repeat仁堪,后面還要執(zhí)行它。
SequenceAction是怎么知道某一個子動作已執(zhí)行完呢填渠?它實現(xiàn)了ActionCallback弦聂,并將子動作的whoToNotify指向自己,當子動作完成的時候就會調(diào)用自己的actionDone氛什,它就會進入下一個動作莺葫。
這里的SequenceAction和MoveToAction看似是包含與被包含的關(guān)系,實際上它們都是ObjAction的子類屉更。這就是一種組合模式徙融,這樣做的好處是它們的管理者ActionManager不需要區(qū)分誰是組合動作誰是單一動作,統(tǒng)統(tǒng)當作“動作(也就是ObjAction)”來處理瑰谜。
回憶我們在以前的文章說過欺冀,GameObject的父子關(guān)系也是一種組合模式。不管是組合的GameObject還是單一的GameObject萨脑,我們都可以當作普通的GameObject來處理(一樣地操作Transform隐轩,一樣地操作其他Component……),是不是與這里的組合模式有一些相似之處渤早?
ActionManager
ActionManager就是管理動作的類职车,它負責讓動作“真正執(zhí)行起來”,并銷毀標記為destroy的動作鹊杖。
public class ActionManager: MonoBehaviour, ActionCallback {
private Dictionary<int, ObjAction> actions = new Dictionary<int, ObjAction>();
private List<ObjAction> waitingToAdd = new List<ObjAction>();
private List<int> watingToDelete = new List<int>();
protected void Update() {
foreach(ObjAction ac in waitingToAdd) {
actions[ac.GetInstanceID()] = ac;
}
waitingToAdd.Clear();
foreach(KeyValuePair<int, ObjAction> kv in actions) {
ObjAction ac = kv.Value;
if (ac.destroy) {
watingToDelete.Add(ac.GetInstanceID());
} else if (ac.enable) {
ac.Update();
}
}
foreach(int key in watingToDelete) {
ObjAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
watingToDelete.Clear();
}
public void addAction(GameObject gameObject, ObjAction action, ActionCallback whoToNotify) {
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.whoToNotify = whoToNotify;
waitingToAdd.Add(action);
action.Start();
}
public void actionDone(ObjAction source) {
}
}
ActionManager實現(xiàn)了MonoBehaviour悴灵,因此它的Update方法在每一幀都會自動被調(diào)用,而在它的Update方法中又調(diào)用了所有已添加的動作的Update骂蓖,這就是為什么一個動作只有添加到了ActionManager才會真正執(zhí)行起來的原因积瞒!我們說過ObjAction本身的Update不會自動被調(diào)用(不是MonoBehaviour的子類),它們需要靠ActionManager來“帶動”登下。
FirstSceneActionManager
FirstSceneActionManager是ActionManager的子類茫孔,F(xiàn)irstController就是通過它來管理所有動作的叮喳。本來有ActionManager似乎已經(jīng)足夠管理動作了,為什么還要實現(xiàn)一個子類FirstSceneActionManager來管理動作呢缰贝?FirstSceneActionManager針對具體的需求做了封裝馍悟,讓FirstController調(diào)用起來更簡潔。
public class FirstSceneActionManager:ActionManager {
public void moveBoat(BoatController boat) {
MoveToAction action = MoveToAction.getAction(boat.getDestination(), boat.movingSpeed);
this.addAction(boat.getGameobj(), action, this);
}
public void moveCharacter(MyCharacterController characterCtrl, Vector3 destination) {
Vector3 currentPos = characterCtrl.getPos();
Vector3 middlePos = currentPos;
if (destination.y > currentPos.y) { //from low(boat) to high(coast)
middlePos.y = destination.y;
} else { //from high(coast) to low(boat)
middlePos.x = destination.x;
}
ObjAction action1 = MoveToAction.getAction(middlePos, characterCtrl.movingSpeed);
ObjAction action2 = MoveToAction.getAction(destination, characterCtrl.movingSpeed);
ObjAction seqAction = SequenceAction.getAction(1, 0, new List<ObjAction>{action1, action2});
this.addAction(characterCtrl.getGameobj(), seqAction, this);
}
}
可以想象剩晴,如果我們不封裝一個FirstSceneActionManager锣咒,而是直接使用ActionManager來管理場景中的動作,那么FirstController中的移動代碼將會有多么臃腫李破!
修改后的FirstController
最后讓我們看看修改后的FirstController是怎么使用動作管理器的:
public class FirstController : MonoBehaviour, SceneController, UserAction {
UserGUI userGUI;
public CoastController fromCoast;
public CoastController toCoast;
public BoatController boat;
private MyCharacterController[] characters;
private FirstSceneActionManager actionManager;
void Awake() {
Director director = Director.getInstance ();
director.currentSceneController = this;
userGUI = gameObject.AddComponent <UserGUI>() as UserGUI;
characters = new MyCharacterController[6];
loadResources ();
}
void Start() {
// FirstController加載時要獲取掛載在該GameObject上的FirstSceneActionManager宠哄!
// 并將它的引用保存到自己的成員變量actionManager 以便后面的調(diào)用。
actionManager = GetComponent<FirstSceneActionManager>();
}
public void loadResources() {
//GameObject water = Instantiate (Resources.Load ("Perfabs/Water", typeof(GameObject)), water_pos, Quaternion.identity, null) as GameObject;
//water.name = "water";
fromCoast = new CoastController ("from");
toCoast = new CoastController ("to");
boat = new BoatController ();
loadCharacter ();
}
private void loadCharacter() {
for (int i = 0; i < 3; i++) {
MyCharacterController cha = new MyCharacterController ("priest");
cha.setName("priest" + i);
cha.setPosition (fromCoast.getEmptyPosition ());
cha.getOnCoast (fromCoast);
fromCoast.getOnCoast (cha);
characters [i] = cha;
}
for (int i = 0; i < 3; i++) {
MyCharacterController cha = new MyCharacterController ("devil");
cha.setName("devil" + i);
cha.setPosition (fromCoast.getEmptyPosition ());
cha.getOnCoast (fromCoast);
fromCoast.getOnCoast (cha);
characters [i+3] = cha;
}
}
public void moveBoat() {
if (boat.isEmpty ())
return;
/* old way to move boat
boat.Move ();
*/
// 這里使用動作管理器取代了我們之前寫的舊方法`凸ァ毛嫉!
actionManager.moveBoat(boat);
boat.move();
userGUI.status = check_game_over ();
}
public void characterIsClicked(MyCharacterController characterCtrl) {
if (characterCtrl.isOnBoat ()) {
CoastController whichCoast;
if (boat.get_to_or_from () == -1) { // to->-1; from->1
whichCoast = toCoast;
} else {
whichCoast = fromCoast;
}
boat.GetOffBoat (characterCtrl.getName());
//characterCtrl.moveToPosition (whichCoast.getEmptyPosition ());
// 這里使用動作管理器取代了我們之前寫的舊方法!妇菱!
actionManager.moveCharacter(characterCtrl, whichCoast.getEmptyPosition ());
characterCtrl.getOnCoast (whichCoast);
whichCoast.getOnCoast (characterCtrl);
} else { // character on coast
CoastController whichCoast = characterCtrl.getCoastController ();
if (boat.getEmptyIndex () == -1) { // boat is full
return;
}
if (whichCoast.get_to_or_from () != boat.get_to_or_from ()) // boat is not on the side of character
return;
whichCoast.getOffCoast(characterCtrl.getName());
//characterCtrl.moveToPosition (boat.getEmptyPosition());
// 這里使用動作管理器取代了我們之前寫的舊方法3性痢!
actionManager.moveCharacter(characterCtrl, boat.getEmptyPosition());
characterCtrl.getOnBoat (boat);
boat.GetOnBoat (characterCtrl);
}
userGUI.status = check_game_over ();
}
int check_game_over() { // 0->not finish, 1->lose, 2->win
int from_priest = 0;
int from_devil = 0;
int to_priest = 0;
int to_devil = 0;
int[] fromCount = fromCoast.getCharacterNum ();
from_priest += fromCount[0];
from_devil += fromCount[1];
int[] toCount = toCoast.getCharacterNum ();
to_priest += toCount[0];
to_devil += toCount[1];
if (to_priest + to_devil == 6) // win
return 2;
int[] boatCount = boat.getCharacterNum ();
if (boat.get_to_or_from () == -1) { // boat at toCoast
to_priest += boatCount[0];
to_devil += boatCount[1];
} else { // boat at fromCoast
from_priest += boatCount[0];
from_devil += boatCount[1];
}
if (from_priest < from_devil && from_priest > 0) { // lose
return 1;
}
if (to_priest < to_devil && to_priest > 0) {
return 1;
}
return 0; // not finish
}
public void restart() {
boat.reset ();
fromCoast.reset ();
toCoast.reset ();
for (int i = 0; i < characters.Length; i++) {
characters [i].reset ();
}
}
}
在我之前的文章里闯团,我是通過掛載MoveableScript在每一個要移動的對象上來讓物體移動起來辛臊,這次我們只要將FirstSceneActionManager掛載到空對象上,然后通過它來控制物體的移動房交。
感謝閱讀彻舰!