物理系统与碰撞
改进飞碟(Hit UFO)游戏
游戏内容要求
- 按adapter模式设计图修改飞碟游戏
- 使它同时支持物理运动和运动学(变换)运动
游戏实现过程
Adapter(适配器)模式简介
适配器模式的定义是,将某个类的接口转换成客户端期望的另一个接口表示。使得原本由于接口不兼容而不能一起工作的那些类能在一起工作。
在这次作业中。我们需要再实现一个基于物理的运动管理器,这样游戏中就同时存在了两个运动控制器。我们要为两个运动控制器创建一个统一的接口,然后场景控制器就可以通过这个统一的接口调用运动控制器,而不需要关心实现这个运动操作的是哪一个运动管理器。
IActionManager统一接口
场景控制器所需要的接口只有一个,也就是需要动作控制器实现Fly函数。Fly函数传入参数为需要执行飞行运动的游戏对象ufo,飞行的方向angle,以及飞行的初速度大小v。我们只需要CCActionManager和PhysisActionManager两个动作管理器中都实现了这个Fly函数,控制器FirstController就可以通过这个接口使飞碟运动,而无所谓实际使用的是哪一个控制器,这样就实现了同时支持物理运动和运动学运动,而且不需要对原来的代码做过多的修改。
public interface IActionManager
{
void Fly(GameObject ufo, float angle, float v);
}
PhysisActionManager
PhysisActionManager运动控制器的实现比起CCActionManager要简单得多(至少我是这么认为)。在PhysisActionManager中首先为游戏对象添加Rigidbody组件,由于Rigidbody默认打开了重力,所以物体运动过程中会自动下坠,我们并不需要像CCActionManager中在Update过程里计算物体的运动轨迹。由于飞碟游戏对象的初始位置已经给定了,所以我们只需要给飞碟一个初速度,飞碟就会自动沿抛物线运动。
public class PhysisActionManager : MonoBehaviour, IActionManager
{
// Start is called before the first frame update
void Start()
{
}
public void Fly(GameObject ufo, float angle, float v)
{
if (ufo.GetComponent<Rigidbody>() == null)
{
ufo.AddComponent<Rigidbody>();
}
float v_x = v * 20f / (float)(Math.Sqrt((400f + angle * angle)));
float v_y = v * angle / (float)(Math.Sqrt((400f + angle * angle)));
if (ufo.GetComponent<UfoProperty>().direction.x == -1)
{
ufo.GetComponent<Rigidbody>().velocity = new Vector3(v_x*-1, v_y, 0);
}
else
{
ufo.GetComponent<Rigidbody>().velocity = new Vector3(v_x, v_y, 0);
}
}
// Update is called once per frame
void Update()
{
}
}
IUserAction
在FirstController中保存了一个标志位用于确定当前使用哪一个运动管理器,而用户需要能够选择运动管理器,并且通过游戏界面显示当前使用的是哪一个游戏管理器。在IUserAction中添加了GetAction和SetAction接口,同时在FirstController中实现了这两个函数。
public interface IUserAction
{
void Hit(Vector3 pos);
int GetScore();
int GetLife();
int GetRound();
int GetAction();
void ReStart();
void SetAction(int type);
}
UserGUI部分
用户交互部分添加了修改运动管理器和显示当前运动管理器种类的功能。
void OnGUI ()
{
GUISkin skin = GUI.skin;
skin.button.normal.textColor = Color.black;
skin.label.normal.textColor = Color.black;
skin.button.fontSize = 20;
skin.label.fontSize = 20;
GUI.skin = skin;
int life = action.GetLife();
if (Input.GetButtonDown("Fire1"))
action.Hit(Input.mousePosition);
if(life > 0)
{
string life_string = "";
for (int i = 0; i < life; i++) life_string += "#";
GUI.Label(new Rect(0, 0, Screen.width / 8, Screen.height / 16), "Life:" + life_string);
GUI.Label(new Rect(0, Screen.height / 16, Screen.width / 8, Screen.height / 16), "Score:" + action.GetScore().ToString());
GUI.Label(new Rect(0, Screen.height / 8, Screen.width / 8, Screen.height / 16), "Round:" + action.GetRound().ToString());
GUI.Label(new Rect(0, Screen.height * 3 / 16, Screen.width / 4, Screen.height / 16), "Action manager:"+ action.GetAction().ToString());
}
else
{
GUI.Label(new Rect(Screen.width/2-60, Screen.height*5/16, 120, Screen.height / 8), "Game Over!");
GUI.Label(new Rect(Screen.width/2-75, Screen.height*7/16, 150, Screen.height / 8), "Your score is:"+ action.GetScore().ToString());
if (GUI.Button(new Rect(Screen.width * 3 / 8, Screen.height * 9 / 16, Screen.width / 4, Screen.height / 8), "ActionManager1"))
{
action.SetAction(1);
action.ReStart();
return;
}
if (GUI.Button(new Rect(Screen.width * 3 / 8, Screen.height * 11 / 16, Screen.width / 4, Screen.height / 8), "ActionManager2"))
{
action.SetAction(2);
action.ReStart();
return;
}
}
}
FirstController部分
FirstController在开始时同时创建两个运动管理器,由于两种管理器都实现了IActionManager接口,所以CCActionManager和PhysisActionManager都可以转换为IActionManager类。在之后发射飞碟的时候,只需要根据当前的标志位(由userGUI控制)选择控制器调用Fly函数就可以了。
void Start ()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton<Factory>.Instance;
judgement = Singleton<CCJudgement>.Instance;
action_manager1 = gameObject.AddComponent<CCActionManager>() as IActionManager;
action_manager2 = gameObject.AddComponent<PhysisActionManager>() as IActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
游戏运行效果:
在游戏运行界面上添加了显示当前运动管理器的部分。
玩家在游戏重新开始时,可以选择使用哪一种ActionManager,ActionManager1是运动学运动管理器,ActionManager2是物理运动管理器。
详细工程代码请查看我的github:https://github.com/Eric3778/UFO/tree/HW5,如果有什么问题请大家及时指出,谢谢。
打靶游戏(可选作业)
游戏要求
- 靶对象为 5 环,按环计分;
- 箭对象,射中后要插在靶上
- 增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
- 游戏仅一轮,无限 trials;
- 增强要求:添加一个风向和强度标志,提高难度
游戏实现过程
游戏对象
使用五个堆叠的同心扁平圆锥来模拟靶子:
使用一个胶囊体模拟箭,效果如下:
UserGUI
由于这个游戏中不需要设置不同的难度,也不需要计算生命值,所以用户界面上就只有当前分数的显示,以及一个重新游戏按钮。同时用户点击界面会调用FirstController的Shoot函数,然后由FirstController创建一个向鼠标位置发射的箭。
public class UserGUI : MonoBehaviour
{
private IUserAction action;
void Start ()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI ()
{
GUISkin skin = GUI.skin;
skin.button.normal.textColor = Color.black;
skin.label.normal.textColor = Color.black;
skin.button.fontSize = 20;
skin.label.fontSize = 20;
GUI.skin = skin;
if (Input.GetMouseButtonDown(0))
{
action.Shoot(Input.mousePosition);
Debug.Log("Input.GetMouseButtonDown response");
}
GUI.Label(new Rect(0, Screen.height / 16, Screen.width / 8, Screen.height / 16), "Score:" + action.GetScore().ToString());
if (GUI.Button(new Rect(Screen.width * 3 / 8, 0, Screen.width / 4, Screen.height / 8), "Restart"))
{
action.ReStart();
return;
}
}
void OnMouseDown()
{
action.Shoot(Input.mousePosition);
}
}
PhysisActionManager
在这个游戏中我只使用了基于物理的运动控制器。Fly函数实际上就是给arrow一个初始速度,接下来arrow就会在重力作用下沿着一个抛物线射向前方。
public class PhysisActionManager : MonoBehaviour, IActionManager
{
// Start is called before the first frame update
void Start()
{
}
public void Fly(GameObject arrow)
{
if (arrow.GetComponent<Rigidbody>() == null)
{
arrow.AddComponent<Rigidbody>();
}
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 20);
}
// Update is called once per frame
void Update()
{
}
}
FirstController
FirstController中主要实现的就是Shoot函数,传入鼠标点击位置,然后在对应位置创建一个Arrow游戏对象,最后传入Fly函数发射arrow。
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public IActionManager action_manager;
public UserGUI user_gui;
public CCJudgement judgement;
private int count;
void Start ()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
judgement = Singleton<CCJudgement>.Instance;
action_manager = gameObject.AddComponent<PhysisActionManager>() as IActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
count = 0;
}
void Update ()
{
if(count < 20)
count += 1;
}
public void LoadResources()
{
}
public void Shoot(Vector3 pos)
{
if (count < 20) return;
count = 0;
Debug.Log(pos);
Quaternion rotations = Quaternion.identity;
rotations.eulerAngles = new Vector3(90f, 0f, 0f);
Vector3 start_pos = new Vector3((Input.mousePosition.x - Screen.width / 2f) *0.02f, (Input.mousePosition.y - Screen.height / 2f)*0.02f, -10);
GameObject ufo_object = Instantiate(Resources.Load<GameObject>("Prefabs/arrow"), start_pos , rotations);
action_manager.Fly(ufo_object);
}
public void AddScore(int score)
{
judgement.Record(score);
}
public int GetScore()
{
return judgement.score;
}
public void ReStart()
{
judgement.Reset();
}
}
ArrowCollision
每个arrow游戏对象都包含一个碰撞检测的类ArrowCollision。在OnCollisionEnter函数中,当arrow撞到一个物体时,被撞物体会作为参数传入,这个时候就通过被撞物体名来检测arrow射中了哪一环。检测出射中哪一环之后再调用CCJudgement裁判类中的计分函数。
public class ArrowCollision : MonoBehaviour
{
private bool stop;
private FirstController action;
// Start is called before the first frame update
void Start()
{
stop = false;
action = SSDirector.GetInstance().CurrentScenceController as FirstController;
}
// Update is called once per frame
void Update()
{
}
void OnCollisionEnter(Collision collision)
{
Debug.Log(collision.gameObject.name);
if (stop) return;
stop = true;
if (collision.gameObject.name == "roll5")
action.AddScore(1);
else if (collision.gameObject.name == "roll4")
action.AddScore(2);
else if (collision.gameObject.name == "roll3")
action.AddScore(3);
else if (collision.gameObject.name == "roll2")
action.AddScore(4);
else if (collision.gameObject.name == "roll1")
action.AddScore(5);
Destroy(this.GetComponent<Rigidbody>());
}
}
游戏运行效果: