改进飞碟游戏
要求
改进飞碟(Hit UFO)游戏:
- 按 adapter模式设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(变换)运动
设计与实现
这次我们需要按照Adapter模式重写飞碟的控制逻辑,可以同时支持物理运动和运动学变换运动。
我们新定义一个接口IActionManager
,使得我们可以自定义PlayDisk
方法的逻辑实现:
public interface IActionManager
{
void PlayDisk(Disk Disk);
bool IsAllFinished();
}
我们在原来运动的基础上,添加了一个CCPhysisActionManager
类,用以管理物理运动,这也是本次要实现的支持物理运动部分的代码:
public class CCPhysisActionManager : SSActionManager, SSActionCallback, IActionManager
{
int count = 0;
public SSActionEventType Complete = SSActionEventType.Completed;
UserAction UserActionController;
public void PlayDisk(Disk Disk)
{
Debug.Log("CCPhysisActionManager");
count ++;
Complete = SSActionEventType.Started;
CCPhysisAction action = CCPhysisAction.getAction(Disk.speed);
addAction(Disk.gameObject, action, this);
}
public void SSActionCallback(SSAction source, bool isHit)
{
count --;
Complete = SSActionEventType.Completed;
UserActionController = SSDirector.getInstance().currentScenceController as UserAction;
if (!isHit)
{
UserActionController.ReduceHealth();
}
source.gameObject.SetActive(false);
}
public bool IsAllFinished()
{
Debug.Log("isALLFInished");
if (count == 0)
return true;
else return false;
}
}
底层的物理运动类CCPhysisAction
的实现如下所示:
public class CCPhysisAction : SSAction
{
public float speedx;
private CCPhysisAction() {}
public static CCPhysisAction getAction(float speedx)
{
CCPhysisAction action = CreateInstance<CCPhysisAction>();
action.speedx = speedx;
return action;
}
// Use this for initialization
public override void Start()
{
if (!this.gameObject.GetComponent<Rigidbody>())
{
this.gameObject.AddComponent<Rigidbody>();
}
this.gameObject.GetComponent<Rigidbody>().AddForce(Vector3.up * 9.8f * 0.6f, ForceMode.Acceleration);
this.gameObject.GetComponent<Rigidbody>().AddForce(new Vector3(speedx, 0, 0), ForceMode.VelocityChange);
}
// Update is called once per frame
override public void Update()
{
if (transform.position.z == -1)
{
Debug.Log("Hit");
destroy = true;
CallBack.SSActionCallback(this, true);
}
else if (transform.position.y <= -45)
{
Debug.Log("Missing");
Destroy(this.gameObject.GetComponent<Rigidbody>());
destroy = true;
CallBack.SSActionCallback(this, false);
}
}
}
最后我们只需要重新修改场记FirstSceneController
即可:
public class FirstSceneController : MonoBehaviour, ISceneController, UserAction
{
int score = 0;
int round = 1;
int tral = 0;
int health = 5;
bool start = false;
bool gameOver = false;
public bool PhysicManager = false;
bool ManagerofNow = false;
IActionManager Manager;
DiskFactory DF;
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentScenceController = this;
DF = DiskFactory.DF;
//Manager = GetComponent<CCActionManager>();
ManagerofNow = PhysicManager;
if (PhysicManager)
{
Manager = this.gameObject.AddComponent<CCPhysisActionManager>() as IActionManager;
}
else
{
Manager = this.gameObject.AddComponent<CCActionManager>() as IActionManager;
}
}
// Use this for initialization
void Start()
{
}
// Update is called once per frame
int count = 0;
void Update()
{
if (health <= 0)
gameOver = true;
if (gameOver)
return;
if (start == true)
{
count ++;
if (count >= 80)
{
count = 0;
if (DF == null)
{
Debug.LogWarning("DF is NUll!");
return;
}
tral ++;
Disk d = DF.GetDisk(round);
if (Manager == null)
{
Debug.LogWarning("Manager is NULL!");
return;
}
Manager.PlayDisk(d);
//Manager.MoveDisk(d);
if (tral == 10)
{
round ++;
tral = 0;
}
}
}
}
public void LoadResources()
{
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i ++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<Disk>() != null)
{
Color c = hit.collider.gameObject.GetComponent<Renderer>().material.color;
if (c == Color.yellow)
score += 1;
if (c == Color.red)
score += 2;
if (c == Color.black)
score += 3;
GameObject explosion = Instantiate(Resources.Load<GameObject>("Prefabs/ParticleSystem"), hit.collider.gameObject.transform.position, Quaternion.identity);
explosion.GetComponent<ParticleSystem>().Play();
Object.Destroy(explosion, 0.1f);
hit.collider.gameObject.transform.position = new Vector3(0, -400, -1);
}
}
}
public int GetScore()
{
return score;
}
public int GetRound()
{
return round;
}
public int GetHealth()
{
return health;
}
public void ReduceHealth()
{
health -= 1;
if (health < 0)
health = 0;
}
public void GameOver()
{
gameOver = true;
}
public bool RoundStop()
{
if (round > 3 || health <= 0)
{
start = false;
return Manager.IsAllFinished();
}
else
return false;
}
public void Restart()
{
score = 0;
round = 1;
health = 5;
start = true;
gameOver = false;
}
}
在主界面中,要切换物理运动,我们只需要勾选我们定义的PhysicManager
变量为真即可:
在控制台的信息中,我们可以看到我们正在使用物理运动:
打靶游戏
要求
- 靶对象为 5 环,按环计分
- 箭对象,射中后要插在靶上
- 射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
- 游戏仅一轮,无限 trials
- 添加一个风向和强度标志,提高难度
设计与实现
本次打靶游戏主要的难点在于设计物理碰撞,当箭触碰到对应的圆环时,要相应的加上对应的分数,为了实现这一目标,我们首先创建靶的预制:
我们设计了五个圆柱体为靶的子对象,为了标明它们各自的分数,我们使用了Ring Data
类来进行表示,这样在代码中我们就可以获取它们各自的分数了。然后为了接收和箭的物理碰撞,我们给每个圆柱体加上了Mesh Collider
的属性,并添加上了碰撞检测的代码:
public class CollisionDetection : MonoBehaviour
{
public FirstSceneController sceneController;
public ScoreRecorder recorder;
void Start()
{
sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
recorder = Singleton<ScoreRecorder>.Instance;
}
void OnTriggerEnter(Collider arrowHead)
{
Debug.Log("TriggerEnter");
Transform arrow = arrowHead.gameObject.transform.parent;
if (arrow == null)
{
Debug.Log("arrow is null");
return;
}
if (arrow.tag == "arrow")
{
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
arrow.GetComponent<Rigidbody>().isKinematic = true;
recorder.Record(this.gameObject);
arrowHead.gameObject.gameObject.SetActive(false);
arrow.tag = "hit";
}
}
}
对箭来说,我们同样添加了一个Capsule Collider
的属性,并添加了刚体属性:
然后就是主程序了,这部分还是利用了工厂模式生产箭,运动部分添加了风向的影响:
public class ArrowFlyAction : SSAction
{
public Vector3 force;
public Vector3 wind;
private ArrowFlyAction() { }
public static ArrowFlyAction GetSSAction(Vector3 wind)
{
ArrowFlyAction action = CreateInstance<ArrowFlyAction>();
action.force = new Vector3(0, 0, 20);
action.wind = wind;
return action;
}
public override void Update() { }
public override void FixedUpdate()
{
this.gameobject.GetComponent<Rigidbody>().AddForce(wind, ForceMode.Force);
if (this.transform.position.z > 35 || this.gameobject.tag == "hit")
{
this.destroy = true;
this.callback.SSActionEvent(this, this.gameobject);
}
}
public override void Start()
{
gameobject.transform.parent = null;
gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;
gameobject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
}
}
我们还要求在打中靶之后,箭有一个颤抖效果,这个同样使用运动类来实现:
public class ArrowTremble : SSAction
{
float radian = 0;
float perRadian = 3f;
float radius = 0.01f;
Vector3 position;
public float leftTime = 0.8f;
private ArrowTremble() { }
public override void Start()
{
position = transform.position;
}
public static ArrowTremble GetSSAction()
{
ArrowTremble action = CreateInstance<ArrowTremble>();
return action;
}
public override void Update()
{
leftTime -= Time.deltaTime;
if (leftTime <= 0)
{
transform.position = position;
this.destroy = true;
this.callback.SSActionEvent(this);
}
radian += perRadian;
float dy = Mathf.Cos(radian) * radius;
transform.position = position + new Vector3(0, dy, 0);
}
public override void FixedUpdate() {}
}
运动管理类的实现可参照上几个游戏的实现,这里就不再列出了。
我们还是使用工厂模式生产箭支:
public class ArrowFactory : MonoBehaviour
{
public GameObject arrow = null;
private List<GameObject> used = new List<GameObject>();
private Queue<GameObject> free = new Queue<GameObject>();
public FirstSceneController sceneControler;
public GameObject GetArrow()
{
if (free.Count == 0)
{
arrow = Instantiate(Resources.Load<GameObject>("Prefabs/Arrow"));
}
else
{
arrow = free.Dequeue();
if (arrow.tag == "hit")
{
arrow.GetComponent<Rigidbody>().isKinematic = false;
arrow.transform.GetChild(0).gameObject.SetActive(true);
arrow.tag = "arrow";
}
arrow.gameObject.SetActive(true);
}
sceneControler = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
Transform temp = sceneControler.bow.transform.GetChild(1);
arrow.transform.position = temp.transform.position;
arrow.transform.parent = sceneControler.bow.transform;
used.Add(arrow);
return arrow;
}
public void FreeArrow(GameObject arrow)
{
for (int i = 0; i < used.Count; ++ i)
{
if (arrow.GetInstanceID() == used[i].gameObject.GetInstanceID())
{
used[i].gameObject.SetActive(false);
free.Enqueue(used[i]);
used.Remove(used[i]);
break;
}
}
}
}
并使用单例模式实现场景单实例:
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
本次打靶游戏还有一个地方需要注意,那就是子摄像机的实现。如果我们只使用一个摄像机,那么会因为距离太远,看不清楚自己击中了几环,会影响游戏性。所以我们在离靶子比较近的距离上添加了一个子摄像机,并暂时设置成隐藏的效果,当箭距离靶子一定距离之后我们就进行展示,挂载代码如下:
public class ChildCamera : MonoBehaviour
{
public bool isShow = false;
public float leftTime;
void Update()
{
if (isShow)
{
leftTime -= Time.deltaTime;
if (leftTime <= 0)
{
this.gameObject.SetActive(false);
isShow = false;
}
}
}
public void StartShow()
{
this.gameObject.SetActive(true);
isShow = true;
leftTime = 2f;
}
}
为了不让子摄像机全屏显示,我们还设置了其显示的区域:
设置了子摄像机之后,我们也需要设置主摄像机,让主摄像机跟随弓的移动而移动:
public class CameraFlow : MonoBehaviour
{
public GameObject bow;
public float smothing = 5f;
Vector3 offset;
void Start()
{
offset = transform.position - bow.transform.position;
}
void FixedUpdate()
{
Vector3 target = bow.transform.position + offset;
transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
}
}
在整个游戏的过程中,我们新添加一个记分员的类来帮助我们统计游戏分数:
public class ScoreRecorder : MonoBehaviour
{
public int score;
public int targetScore;
public int arrowNumber;
void Start()
{
score = 0;
targetScore = 15;
arrowNumber = 10;
}
public void Record(GameObject gameObject)
{
int temp = gameObject.GetComponent<RingData>().score;
score = temp + score;
}
}
在UI界面,为了更形象的展示我们剩余的箭支数量,我们可以使用字符’I’来进行展示,Gui代码如下:
public class UserGUI : MonoBehaviour
{
private IUserAction action;
GUIStyle score_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle bold_style = new GUIStyle();
GUIStyle over_style = new GUIStyle();
private bool game_start = false;
// Use this for initialization
void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
text_style.normal.textColor = new Color(0, 0, 0, 1);
text_style.fontSize = 16;
score_style.normal.textColor = new Color(1, 0, 0, 1);
score_style.fontSize = 16;
bold_style.normal.textColor = new Color(1, 0, 0);
bold_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 1, 1);
over_style.fontSize = 25;
}
void Update()
{
if (game_start && !action.GetGameover())
{
if (Input.GetButtonDown("Fire1"))
{
action.Shoot();
}
float translationY = Input.GetAxis("Vertical");
float translationX = Input.GetAxis("Horizontal");
action.MoveBow(translationX, translationY);
}
}
private void OnGUI()
{
if (game_start)
{
if (!action.GetGameover())
{
GUI.Label(new Rect(10, 5, 200, 50), "Score:", text_style);
GUI.Label(new Rect(65, 5, 200, 50), action.GetScore().ToString(), score_style);
GUI.Label(new Rect(Screen.width / 2 - 40, 8, 200, 50), "Target score:", text_style);
GUI.Label(new Rect(Screen.width / 2 + 60, 8, 200, 50), action.GetTargetScore().ToString(), score_style);
GUI.Label(new Rect(Screen.width - 170, 5, 50, 50), "Arrows:", text_style);
for (int i = 0; i < action.GetResidueNum(); ++ i)
{
GUI.Label(new Rect(Screen.width - 110 + 10 * i, 5, 50, 50), "I ", bold_style);
}
GUI.Label(new Rect(Screen.width - 170, 30, 200, 50), "winds: ", text_style);
GUI.Label(new Rect(Screen.width - 110, 30, 200, 50), action.GetWind(), text_style);
}
if (action.GetGameover())
{
GUI.Label(new Rect(Screen.width / 2 - 65, Screen.width / 2 - 250, 100, 100), "Game Over", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 170, 100, 50), "Restart"))
{
action.Restart();
return;
}
}
}
else
{
GUI.Label(new Rect(Screen.width / 2 - 40, Screen.width / 2 - 320, 100, 100), "Archery", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 170, 100, 50), "Game Start"))
{
game_start = true;
action.BeginGame();
}
}
}
}
我们的场记以及接口的实现如下所示:
public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController
{
public Camera mainCamera;
public Camera childCamera;
public ScoreRecorder recorder;
public ArrowFactory arrowFactory;
public ArrowFlyActionManager actionManager;
public GameObject bow;
private GameObject arrow;
private GameObject target;
private int[] targetscore = { 15, 30, 40, 50 };
private int round = 0;
private int arrowNum = 0;
private List<GameObject> store = new List<GameObject>();
private bool gameOver = false;
private bool gameStart = false;
private string wind = "";
private float windDirectX;
private float windDirectY;
void Start()
{
SSDirector director = SSDirector.GetInstance();
arrowFactory = Singleton<ArrowFactory>.Instance;
recorder = Singleton<ScoreRecorder>.Instance;
director.CurrentScenceController = this;
actionManager = gameObject.AddComponent<ArrowFlyActionManager>() as ArrowFlyActionManager;
LoadResources();
mainCamera.GetComponent<CameraFlow>().bow = bow;
windDirectX = Random.Range(-1, 1);
windDirectY = Random.Range(-1, 1);
CreateWind();
}
void Update()
{
if (gameStart)
{
for (int i = 0; i < store.Count; i++)
{
GameObject temp = store[i];
if (temp.transform.position.z > 35 || store.Count > 5)
{
arrowFactory.FreeArrow(store[i]);
store.Remove(store[i]);
}
}
}
}
public void LoadResources()
{
bow = Instantiate(Resources.Load("Prefabs/Bow", typeof(GameObject))) as GameObject;
target = Instantiate(Resources.Load("Prefabs/Target", typeof(GameObject))) as GameObject;
}
public void MoveBow(float offsetX, float offsetY)
{
if (gameOver || !gameStart)
return;
if (bow.transform.position.x > 5)
{
bow.transform.position = new Vector3(5, bow.transform.position.y, bow.transform.position.z);
return;
}
else if (bow.transform.position.x < -5)
{
bow.transform.position = new Vector3(-5, bow.transform.position.y, bow.transform.position.z);
return;
}
else if (bow.transform.position.y < -3)
{
bow.transform.position = new Vector3(bow.transform.position.x, -3, bow.transform.position.z);
return;
}
else if (bow.transform.position.y > 5)
{
bow.transform.position = new Vector3(bow.transform.position.x, 5, bow.transform.position.z);
return;
}
offsetY *= Time.deltaTime;
offsetX *= Time.deltaTime;
bow.transform.Translate(offsetX, 0, 0);
bow.transform.Translate(0, offsetY, 0);
}
public void Shoot()
{
if ((!gameOver || gameStart) && arrowNum <= 10)
{
arrow = arrowFactory.GetArrow();
store.Add(arrow);
Vector3 wind = new Vector3(windDirectX, windDirectY, 0);
actionManager.ArrowFly(arrow, wind);
childCamera.GetComponent<ChildCamera>().StartShow();
recorder.arrowNumber--;
arrowNum++;
}
}
public void CheckGamestatus()
{
if (recorder.arrowNumber <= 0 && recorder.score < recorder.targetScore)
{
gameOver = true;
return;
}
else if (recorder.arrowNumber <= 0 && recorder.score >= recorder.targetScore)
{
round++;
arrowNum = 0;
if (round == 4)
gameOver = true;
for (int i = 0; i < store.Count; ++i)
{
arrowFactory.FreeArrow(store[i]);
}
store.Clear();
recorder.arrowNumber = 10;
recorder.score = 0;
recorder.targetScore = targetscore[round];
}
windDirectX = Random.Range(-(round + 1), (round + 1));
windDirectY = Random.Range(-(round + 1), (round + 1));
CreateWind();
}
public void CreateWind()
{
string Horizontal = "", Vertical = "", level = "";
if (windDirectX > 0)
{
Horizontal = "west";
}
else if (windDirectX <= 0)
{
Horizontal = "east";
}
if (windDirectY > 0)
{
Vertical = "South";
}
else if (windDirectY <= 0)
{
Vertical = "North";
}
if ((windDirectX + windDirectY) / 2 > -1 && (windDirectX + windDirectY) / 2 < 1)
{
level = "LV1";
}
else if ((windDirectX + windDirectY) / 2 > -2 && (windDirectX + windDirectY) / 2 < 2)
{
level = "LV2";
}
else if ((windDirectX + windDirectY) / 2 > -3 && (windDirectX + windDirectY) / 2 < 3)
{
level = "LV3";
}
else if ((windDirectX + windDirectY) / 2 > -5 && (windDirectX + windDirectY) / 2 < 5)
{
level = "LV4";
}
wind = Vertical + Horizontal + " " + level;
}
public void BeginGame()
{
gameStart = true;
}
public bool GetGameover()
{
return gameOver;
}
public int GetScore()
{
return recorder.score;
}
public int GetTargetScore()
{
return recorder.targetScore;
}
public int GetResidueNum()
{
return recorder.arrowNumber;
}
public string GetWind()
{
return wind;
}
public void Restart()
{
gameOver = false;
recorder.arrowNumber = 10;
recorder.score = 0;
recorder.targetScore = 15;
round = 0;
arrowNum = 0;
for (int i = 0; i < store.Count; ++ i)
{
arrowFactory.FreeArrow(store[i]);
}
store.Clear();
}
}
导演类的实现还是一样:
public class SSDirector : System.Object
{
private static SSDirector _instance; //导演类的实例
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
最后,为了美观效果,我们给我们的游戏添加一个简单的天空盒,可以直接到商店界面下载免费资源;为了让游戏更真实,我们在下方加入草地,模拟游戏的场地效果;最后游戏的界面效果如下:
当箭飞到离靶一定距离的时候,右下方就可以显示我们的子摄像机,让玩家可以清楚地看见自己射箭的结果如何,让他们清楚下一箭该如何调整位置。