UML图
用的依然是第一次作业就在用的祖传架构,不过这次可以整体复用的比较少,只有 IsceneController,Director和singleton。其他类都需要做出较大的改动。
动画编辑
玩家,也就是rooster的动画逻辑比较简单,一直奔跑就可以
巡逻兵,也就是Crow的动画逻辑如下:
其中从Fly到Attack需要一个trigger类型的变量,在Crow捕捉到rooster的时候设置即可,相关代码如下:
if (name.Substring(0,6) == "rooste") {
m_Animator.SetTrigger("fly_attack");
}
代码实现
本次作业我采用了自底向上的方式,首先实现了较为底层的Moveable和Patrolable。Moveable和Patrolable相当于两种功能,若gameObject挂载上Moveable就可以控制WASD控制行动;Patrolable则是由巡逻兵挂载上,以实现自动巡逻以及追击功能。
Moveable 核心代码如下:
if (Input.GetKey(KeyCode.W))
{
m_Animator.SetTrigger("standMove");
m_transform.rotation = Quaternion.Euler(0, 180, 0);
zm += m_movSpeed * Time.deltaTime;
}
else if (Input.GetKey(KeyCode.S))//按键盘S向下移动
{
m_Animator.SetTrigger("standMove");
m_transform.rotation = Quaternion.Euler(0, 0, 0);
zm -= m_movSpeed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.A))//按键盘A向左移动
{
m_Animator.SetTrigger("standMove");
m_transform.rotation = Quaternion.Euler(0, 90, 0);
xm -= m_movSpeed * Time.deltaTime;
}
else if (Input.GetKey(KeyCode.D))//按键盘D向右移动
{
m_Animator.SetTrigger("standMove");
m_transform.rotation = Quaternion.Euler(0, 270, 0);
xm += m_movSpeed * Time.deltaTime;
}
m_transform.position = m_transform.position + new Vector3 (xm, ym, zm);
Patrolable的实现相对复杂。为了实现巡逻的目的,首先要构造一个矩形,并在矩形上随机选取3~5个点。为了实现这个目的,我采取的策略是先选取三个点,若随机选取的点数大于三,再选取剩余的点:
void SetPatrolPoints(){
patrolPoints [0] = transform.position;
patrolPoints[1] = patrolPoints [0] + new Vector3(Random.Range(0.0f,domainWidth),0.5f,-domainWidth/2);
patrolPoints[2] = patrolPoints [0] + new Vector3(domainWidth,0.5f,Random.Range(-domainWidth/2,domainWidth/2));
if (pointNum >= 4) {
patrolPoints[3] = patrolPoints [0] + new Vector3(Random.Range(0.0f,domainWidth),0.5f,domainWidth/2);
}
if (pointNum == 5){
patrolPoints[4] = patrolPoints [0] + new Vector3(0,0.5f,domainWidth/2);
}
}
选取好点之后,根据反三角函数算出从当前点到下一个点运动时Crow需要旋转的角度,计算方式如下:
float Angle(Vector3 cen, Vector3 first, Vector3 second)
{
float dx1, dx2, dy1, dy2, dx3, dy3;
float angle;
dx1 = first.x - cen.x;
dy1 = first.z - cen.z;
dx2 = second.x - cen.x;
dy2 = second.z - cen.z;
dx3 = second.x - first.x;
dy3 = second.z - first.z;
float a = (float)Mathf.Sqrt(dx1 * dx1 + dy1 * dy1);
float b = (float)Mathf.Sqrt(dx2 * dx2 + dy2 * dy2);
float c =(float)Mathf.Sqrt(dx3 * dx3 + dy3 * dy3);
if (dx1 == 0 && dy1 < 0) return 180;
//angle = (float)Mathf.Acos((dx1 * dx2 + dy1 * dy2) / c);
angle = (float)Mathf.Acos((a*a + b*b -c*c) / (2*a*b));
if (angle == null)
return 180;
return (angle *180) / 3.14159f ;
}
随后就可以通过MoveTowards前往下一个点了。
注意还需要检查是否撞墙,撞墙后巡逻顺序要反过来:
void CheckHitWall(){
reverse = (reverse + 1) % 2;
int temp = curPoint;
curPoint = nextPoint;
nextPoint = temp;
}
整体的巡逻函数如下:
void Patrol(){
GetComponent<Rigidbody>().velocity = Vector3.zero;
GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
if (this.transform.position == patrolPoints [nextPoint]) {
if(reverse == 0){
if(nextPoint == pointNum-1){
curPoint = nextPoint;
nextPoint = 0;
}else{
curPoint = nextPoint;
nextPoint ++;
}
}
else{
if(nextPoint == 0){
curPoint = nextPoint;
nextPoint = pointNum -1;
}
curPoint = nextPoint;
nextPoint --;
}
float yRotation = Angle(transform.position,patrolPoints [nextPoint],new Vector3(transform.position.x,0.5f,transform.position.z+1));
if(patrolPoints [nextPoint].x < transform.position.x) yRotation = -yRotation;
transform.rotation = Quaternion.Euler(0, yRotation, 0);
}
float step = speed * Time.deltaTime;
// check if find enemy
if (findEnemy) {
float yRotation = Angle (transform.position, enemyPos, new Vector3 (transform.position.x, 0.5f, transform.position.z + 1));
if (enemyPos.x < transform.position.x)
yRotation = -yRotation;
transform.rotation = Quaternion.Euler (0, yRotation, 0);
transform.position = Vector3.MoveTowards (transform.position, enemyPos, step);
} else {
float yRotation = Angle(transform.position,patrolPoints [nextPoint],new Vector3(transform.position.x,0.5f,transform.position.z+1));
if(patrolPoints [nextPoint].x < transform.position.x) yRotation = -yRotation;
transform.rotation = Quaternion.Euler(0, yRotation, 0);
transform.position = Vector3.MoveTowards (transform.position, patrolPoints[nextPoint], step);
}
// check if hit wall
//CheckHitWall ();
// move to next point
}
有了Patrolable函数后,可以挂载到一个Crow实例上查看一下效果,发现可以正常巡逻后再来实现Crow类。
Crow类主要管理crow实例的初始位置,Patrolable组件也是在这里添加上的。另外在这个类初始化的时候根据乌鸦所处的位置设置了行动速度,越深入乌鸦运行速度越快,游戏也就越困难。
public class Crow{
public GameObject crow;
public Vector3 startPoint;
public Patrolable pt;
public Crow(float x,float y){
haveFoundEnemy = false;
startPoint = new Vector3 (10 * x, 0.5f, 10 * y);
crow = Object.Instantiate((GameObject)UnityEditor.AssetDatabase.LoadAssetAtPath ("Assets/Prefabs/crow.prefab", typeof(GameObject)),startPoint , Quaternion.identity) as GameObject;
//crow = Object.Instantiate(Resources.Load("Assets/Prefabs/crow"),new Vector3(0,0,0), Quaternion.identity);
pt = crow.AddComponent(typeof(Patrolable)) as Patrolable;
if (x >= 3.0f) {
pt.SetSpeed(6);
}
}
}
CrowFactory初始时维护一个空链表,GetCrow()函数会填充这个空链表并返回。
public class Crow{
public GameObject crow;
public Vector3 startPoint;
public Patrolable pt;
public Crow(float x,float y){
haveFoundEnemy = false;
startPoint = new Vector3 (10 * x, 0.5f, 10 * y);
crow = Object.Instantiate((GameObject)UnityEditor.AssetDatabase.LoadAssetAtPath ("Assets/Prefabs/crow.prefab", typeof(GameObject)),startPoint , Quaternion.identity) as GameObject;
pt = crow.AddComponent(typeof(Patrolable)) as Patrolable;
if (x >= 3.0f) {
pt.SetSpeed(6);
}
}
}
MsgManager管理信息的传递,由于这次老师要求用订阅与发布模式,所以需要这个类居中管理信息的传递。其他类通过单例模式引用这个类传递信息。MsgManager维护需要被传递信息的实例,本次作业中体现为FirstController,因此FirstController必须有接收信息的功能。
public class MsgManager : MonoBehaviour {
private UserAction action;
// Use this for initialization
void Start () {
action = Director.getInstance().currentSceneController as UserAction;
}
void Update(){
if(action == null)
action = Director.getInstance().currentSceneController as UserAction;
}
public void SendMsg(string msg, string from, string to){
if (to == "FC") {
action.ReceiveMessage(msg);
}
}
}
另外本次作业是由挂载Moveable的rooster和挂载Patrolable的Crow来向FirstController传递加分、游戏结束、游戏胜利的信息。举例如下:
public MsgManager msgm;
void Awake(){
···
msgm = Singleton<MsgManager>.Instance;
}
void OnLostEnemy(){
···
msgm.SendMsg ("add score","PT","FC");
}
FirstController实现起来比较简单,大部分代码都用于加载资源。其他例如Restart(),BeginGame()等函数的实现也相对简单。和以往不同的是,FirstController类实现了ReceiveMessage()函数以接收MsgController传来的信息。
public void ReceiveMessage(string msg){
if (msg == "game over") {
Debug.Log ("game over right here");
GameOver ();
} else if (msg == "add score") {
Debug.Log ("add score right here");
Debug.Log (score);
score ++;
} else if (msg == "win") {
GameOver();
}
}