Unity 3d 六、物理系统与碰撞
Homework6-adapter模式的打飞碟游戏
这周的任务主要就是通过给打飞碟游戏加上adapter模式让我们熟悉adapter模式,体会到Apapter模式给我们编程带来的作用,提升我们面向对象的设计技巧。
完成的类
其实在上一周已完成作业的基础上,这一周并不用加什么特别的东西。只需要写出一个基本的ActionManagerTarget的接口(类似上图中的IActionManager), 然后实现一个供场景控制器调用的ActionManagerAdapter,然后实现我们这次需要实现的PhysicsActionManager, 把原来调用FirstSceneActionController的地方换成调用我们新实现的ActionManagerAdapter,那么这周的作业就完成了。下面我们分类来说说
ActionManagerTarget
这个接口里声明的就是我们动作管理器所需的基本函数,以及一个切换动作管理器的功能。通过使用这个统一的接口,我们可以在多个动作管理器之间无缝切换.
public interface ActionManagerTarget
{
int getMode();
void switchActionManager();
void addAction (GameObject ufo, float speed);
void removeActionOf (GameObject ufo);
}
ActionManagerAdapter
这里就是对于我们声明的一个接口的实现,通过实现这个接口以供场景控制器调用,我们给游戏加上的Adapter模式的工作就完成一大半了,剩下的基本就是完成PhysicsActionManager了。
public class ActionManagerAdapter : ActionManagerTarget
{
FirstSceneActionManager firstSceneActionManager;
PhysicsActionManager physicsActionManager;
int mode = 0; // 0: firstSceneActionManager, 1: physicsActionManager
public int getMode()
{
return mode;
}
public void switchActionManager()
{
mode = 1 - mode;
}
public ActionManagerAdapter(GameObject main)
{
firstSceneActionManager = main.AddComponent < FirstSceneActionManager> ();
physicsActionManager = main.AddComponent<PhysicsActionManager> ();
mode = 0;
}
public void addAction(GameObject ufo, float speed)
{
if (mode == 0)
{
firstSceneActionManager.addActionToUFO (ufo, speed);
}
else
{
physicsActionManager.addForceToObj (ufo, speed);
}
}
public void removeActionOf(GameObject ufo)
{
if (mode == 0)
{
firstSceneActionManager.removeActionByObj (ufo);
}
else
{
physicsActionManager.removeForceOfObj (ufo);
}
}
}
PhysicsActionManage
在这里因为给我们的ufo加上了刚体,我们可以直接通过加上力、初速度,去掉力的方式,给动作加上对应的动作,设速度为0的操作实现动作的管理。
public class PhysicsActionManager : MonoBehaviour {
public void addForceToObj (GameObject ufo, float speed)
{
int position = (Random.Range (1, 3) < 2) ? -1 : 1;
ufo.transform.position = new Vector3 (-10 * position, Random.Range (4, 6), 5);
Vector3 speedVector = new Vector3 (Random.Range ((int)speed, (int)(1.5 * speed)) * position, Random.Range ((int)speed, (int)(1.2 * speed)), 0);
ufo.GetComponent<Rigidbody> ().useGravity = true;
ufo.GetComponent<Rigidbody> ().velocity = speedVector;
}
public void removeForceOfObj (GameObject ufo)
{
ufo.GetComponent<Rigidbody> ().useGravity = false;
ufo.GetComponent<Rigidbody> ().velocity = Vector3.zero;
}
}
实现了这些类之后,加上其他代码的一些微小的修整,即一些地方对于切换场景控制器功能的调用的设置(游戏场景内右键点击)以及一些加入了新的PhysicsActionManager后出了一些小Bug的修复和加了一些游戏的彩蛋(飞碟相撞会💥,你打得不快的话有时就打不到了,哈哈),我们就完成这周的作业了,这周的这个作业相对前几周其实任务量并不大,所以,继续肝射箭小游戏去啦🌚!
结果演示
Homework6-射箭小游戏
游戏要求
- 靶对象为 5 环,按环计分;
- 箭对象,射中后要插在靶上
- 增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后(未实现)
- 游戏仅一轮,无限 trials;
- 增强要求:添加一个风向和强度标志,提高难度
UML图
游戏实现
做了这几次的作业以后,我发现基本架构的代码都几乎可以复用,所以我直接把一些基本的每次用到的代码弄了一个BasicCode文件夹,ActionManager也摆放在内,编写游戏的时候,在其基础上增加就好了。
根据我们画出的UML图,我们可以看到我们这次需要完成的类不算多, 基础的代码都有,不不不过这次作业中的PhysicsActionManager,我经过上一次作业的编辑,经过思考,觉得其应该继承我们的SSActionManager,这样不仅可以让我们减少了一个接口类的书写,还把其整合到整个ActionManager的系统中,拥有ActionCallback等的功能,动作的完成的判断可以直接在自身的Update里完成,比起上一次作业优化了挺多。
现在我们分类说说
ArrowFlyAction
这次作业最基本的肯定就是我们箭的动作了,这里我不仅包含了射箭出去的动作,还包含了风向对于箭产生的动作,至于为什么我会把风力对箭的影响实体化成动作在游戏中呢,因为通过使用ActionManager去管理风力,我们可以在场景控制器中减少相应的代码。
ArrowFlyAction实现如下
public class ArrowFlyAction : SSAction {
public int type; // 0: windAction, 1: shootAction
public Vector3 speed;
bool firstTime = false;
private ArrowFlyAction() {}
public static ArrowFlyAction GetSSAction(int type, Vector3 speed) {
ArrowFlyAction arrowFlyAction = ScriptableObject.CreateInstance<ArrowFlyAction> ();
arrowFlyAction.speed = speed;
arrowFlyAction.firstTime = true;
arrowFlyAction.type = type;
return arrowFlyAction;
}
public override void Start() {}
public override void Update() {
if (firstTime) {
if (type == 0) {
// wind
this.gameObject.GetComponent<Rigidbody> ().AddForce (speed, ForceMode.Force);
} else {
// shoot
this.gameObject.GetComponent<Rigidbody> ().AddForce (speed, ForceMode.VelocityChange);
}
firstTime = false;
}
if (this.gameObject.transform.position.z > 50) {
this.destroy = true;
this.callback.actionDone (this);
}
if (this.gameObject.GetComponent<ArrowControl> ().arrowController.available == false) {
this.destroy = true;
}
}
}
PhysicsActionManager
和我一开始说的一样,我的PhysicsActionManager继承了SSActionManager类,并实现了ActionCallback接口以完成事件结束的处理。该类实现挺简单,这里不多BB
public class PhysicsActionManager : SSActionManager, ActionCallback {
public FirstController firstController;
public void Start () {
firstController = (FirstController)GameDirector.getInstance ().currentSceneController;
firstController.physicsActionManager = this;
}
public void Update() {
base.Update ();
}
public void actionDone (SSAction source) {
if (((ArrowFlyAction)source).type == 1) {
firstController.arrowFactory.recycle (((ArrowFlyAction)source).gameObject.GetComponent<ArrowControl> ().arrowController);
firstController.shootFinish = true;
}
}
}
到这里,基础的部分就差不多了,还剩下负责具体功能的各个类与我们的场景控制器。
CheckCrack
我们先来看看我用于检查物体碰撞以提供给计分器,并且使得箭体碰到靶子后插在靶上(停住)的类
这里主要用到了OnTriggerEnter来确定箭体碰到靶子这一事件,只要我们的CheckCrack Add到各个分数对应的GameObject中,我们的靶子就能获取箭射到的位置,交给计分员计分。
在这里,可能因为我靶子实现的不太好,我的箭在射中靶子后,插入靶子里面,从而也会触发后面靶子的触发器,所以这里可以看到,我的ArrowController里加入了一个available,这样保证箭一旦插入就不再处理对应的触发事件。
public class CheckCrack : MonoBehaviour {
public FirstController firstController;
public ScoreRecorder scoreRecorder;
void Start() {
firstController = (FirstController)GameDirector.getInstance ().currentSceneController;
scoreRecorder = ScoreRecorder.getInstance ();
}
void OnTriggerEnter(Collider arrow) {
if (arrow.gameObject.GetComponent<ArrowControl> ().arrowController.available == true) {
arrow.gameObject.GetComponent<Rigidbody> ().velocity = Vector3.zero;
arrow.gameObject.GetComponent<Rigidbody> ().isKinematic = true;
arrow.gameObject.GetComponent<ArrowControl> ().arrowController.available = false;
arrow.gameObject.transform.GetComponent<Collider> ().enabled = false;
firstController.shootFinish = true;
firstController.showCamera.showCamera ();
scoreRecorder.record (gameObject);
}
}
}
ScoreRecorder
计分员的实现和上次类似,不过这一次是根据GameObbject的名字来加分
public class ScoreRecorder {
public int score = 0;
Text gameInfo;
private static ScoreRecorder instance;
public static ScoreRecorder getInstance()
{
if (instance == null)
{
instance = new ScoreRecorder();
}
return instance;
}
private ScoreRecorder() {
gameInfo = (GameObject.Instantiate (Resources.Load ("Prefabs/ScoreInfo")) as GameObject).transform.Find ("Text").GetComponent<Text> ();
gameInfo.text = "" + score;
}
public void record(GameObject hitObj) {
switch (hitObj.name) {
case "1":
score += 1;
break;
case "2":
score += 2;
break;
case "3":
score += 3;
break;
case "4":
score += 4;
break;
case "5":
score += 5;
break;
}
gameInfo.text = "" + score;
}
public int getScore() {
return score;
}
public void reset() {
score = 0;
gameInfo.text = "" + score;
}
}
showSubCamera
这里我加了个子摄像头的功能,若玩家射中靶子,我会激活该摄像头,使玩家清晰看到箭射中的位置
public class showSubCamera : MonoBehaviour {
private float time = 0;
private bool show = false;
GameObject cameraObj;
// Use this for initialization
void Start () {
show = false;
cameraObj = (GameObject.Instantiate (Resources.Load ("Prefabs/SubCamera")) as GameObject);
cameraObj.SetActive (false);
}
public void showCamera() {
cameraObj.SetActive (true);
show = true;
time = 1;
}
// Update is called once per frame
void Update () {
if (show) {
time -= Time.deltaTime;
if (time <= 0) {
show = false;
cameraObj.SetActive (false);
}
}
}
}
WindMake
游戏中我直接实现了一个WindMake类来管理风的生成,每240帧数更换一个风向,并且把其实时的风向显示到游戏画面的左上角
public class WindMake : MonoBehaviour {
public Vector3 Wind;
Text gameInfo;
int fpsCount = 0;
void Awake() {
gameInfo = (GameObject.Instantiate (Resources.Load ("Prefabs/WindInfo")) as GameObject).transform.Find ("Text").GetComponent<Text> ();
makeWind ();
fpsCount = 0;
}
void Update() {
fpsCount++;
if (fpsCount == 240) {
Wind = makeWind ();
fpsCount = 0;
}// 240帧换一个风向
}
public Vector3 makeWind() {
float y = Random.Range (-100, 100);
float z = Random.Range (-30, 0);
float x = Random.Range (-50, 50);
gameInfo.text = "(" + x + "," + y + "," + z + ")";
return new Vector3 (x, y, z);
}
public Vector3 getWind() {
return Wind;
}
}
说到这里,突然看到还有我们的ArrowFactory没有讲🌚
ArrowFactory
实现和基本的Factory类似,没有什么可讲的, 就是加上了对于刚体的初始化等的一些内容
public class ArrowFactory : MonoBehaviour {
Queue<ArrowController> waitingQueue;
List<ArrowController> runningList;
private FirstController firstController;
GameObject basic;
private void Awake() {
waitingQueue = new Queue<ArrowController> ();
runningList = new List<ArrowController> ();
basic = Instantiate (Resources.Load ("Prefabs/Arrow", typeof(GameObject)) as GameObject);
basic.SetActive (false);
}
public ArrowController getArrow() {
ArrowController arrow;
if (waitingQueue.Count == 0) {
GameObject newArrow = GameObject.Instantiate (basic);
arrow = new ArrowController (newArrow);
} else {
arrow = waitingQueue.Dequeue ();
}
arrow.getObject ().transform.parent = ((FirstController)GameDirector.getInstance ().currentSceneController).Bow.transform;
arrow.getObject().GetComponent<Rigidbody> ().velocity = Vector3.zero;
arrow.getObject ().transform.localPosition = new Vector3 (0, 2, 1);
arrow.getObject ().GetComponent<Rigidbody> ().isKinematic = true;
arrow.getObject ().transform.GetComponent<Collider> ().enabled = true;
arrow.appear ();
runningList.Add (arrow);
return arrow;
}
public void recycle(ArrowController arrow) {
arrow.disappear ();
arrow.getObject ().transform.position = new Vector3 (0, 0, -8);
arrow.getObject ().transform.GetComponent<Collider> ().enabled = false;
runningList.Remove (arrow);
waitingQueue.Enqueue (arrow);
}
public void recycleAll() {
while (runningList.Count != 0)
{
recycle(runningList[0]);
}
}
}
ArrowController
这里为了方便Arrow的实现,我在这次作业中也一样实现了ArrowController类,加入了箭体的基本状态的信息,并方便了之后的拓展。
public class ArrowController{
GameObject arrow;
ArrowControl arrowControl;
public bool available = false;
public ArrowController (GameObject gameObject) {
this.arrow = gameObject;
arrowControl = gameObject.AddComponent<ArrowControl> ();
arrowControl.arrowController = this;
}
public void appear()
{
available = true;
arrow.SetActive(true);
}
public void disappear()
{
arrow.SetActive(false);
}
public GameObject getObject() {
return arrow;
}
}
ArrowControl类
这个类主要是用于GameObject获取其对应的Controller
public class ArrowControl : MonoBehaviour {
public ArrowController arrowController;
}
FirstController类
写完各个功能的类后,就剩下我们的游戏场景控制器了,这里基本就是各个类的整合以及用户交互的实现,实现不难。
public class FirstController : MonoBehaviour, ISceneController {
public PhysicsActionManager physicsActionManager;
public GameDirector gameDirector;
public GameObject Bow;
public ArrowController arrow;
public ScoreRecorder scoreRecorder;
public ArrowFactory arrowFactory;
public WindMake windMake;
public showSubCamera showCamera;
GUIStyle gameInfoStyle;
public bool shootFinish = true; // whether arrow hit success
void Awake() {
arrowFactory = gameObject.AddComponent<ArrowFactory> ();
windMake = gameObject.AddComponent<WindMake> ();
physicsActionManager = gameObject.AddComponent<PhysicsActionManager> ();
scoreRecorder = ScoreRecorder.getInstance ();
showCamera = gameObject.AddComponent<showSubCamera> ();
gameDirector = GameDirector.getInstance ();
gameDirector.currentSceneController = this;
}
// Use this for initialization
void Start () {
loadResources ();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
gameInfoStyle = new GUIStyle ();
gameInfoStyle.fontSize = 25;
}
void OnGUI() {
GUI.Label (new Rect (20, 80, 200, 50), "Press space to restart!", gameInfoStyle);
}
// Update is called once per frame
void Update () {
float offsetX = Input.GetAxis ("Mouse X");
float offsetY = Input.GetAxis ("Mouse Y");
moveBow (offsetX, offsetY);
if (Input.GetKeyDown("escape")) {
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
if (Input.GetButton ("Fire1")) {
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
shootArrow ();
}
if (Input.GetKeyDown ("space")) {
reset ();
}
if (shootFinish && arrow == null) {
arrow = arrowFactory.getArrow ();
}
}
public void moveBow(float offsetX, float offsetY) {
float posy = Mathf.Clamp(Bow.transform.position.y + offsetY, 1, 30);
Bow.transform.position = new Vector3 (Bow.transform.position.x + offsetX, posy, Bow.transform.position.z);
}
public void shootArrow() {
if (arrow != null) {
GameObject arrowObj = arrow.getObject ();
arrowObj.GetComponent<Rigidbody> ().isKinematic = false;
Vector3 dir = arrowObj.transform.up * -1;
ArrowFlyAction arrowAction = ArrowFlyAction.GetSSAction (1, dir * 30);
ArrowFlyAction windAction = ArrowFlyAction.GetSSAction (0, windMake.getWind());
physicsActionManager.addAction (arrowObj, arrowAction, physicsActionManager);
physicsActionManager.addAction (arrowObj, windAction, physicsActionManager);
arrowObj.transform.parent = null;
arrow = null;
shootFinish = false;
}
}
public void loadResources() {
Instantiate(Resources.Load("Prefabs/Land"));
Instantiate(Resources.Load("Prefabs/Target"));
Bow = Instantiate(Resources.Load("Prefabs/Bow")) as GameObject;
arrow = arrowFactory.getArrow ();
}
void reset() {
arrow = null;
shootFinish = true;
scoreRecorder.reset ();
arrowFactory.recycleAll ();
}
}
到这里这周的作业就完成了,难度不算太大,在学习了物理引擎的使用后,我们Action的使用更加简单,只需要加上对应的力就可以了,并不需要一些复杂的语句,而且有了之前几周完成的基础的游戏架构,我们基本就是完成负责游戏对象的创建功能的类,完成对象基本动作的类,以及游戏场景中对应功能解耦出来的类。只要有了这些基本的意识后,我们对于写游戏的上手还是很快的。