物理系统与碰撞
PS:所有代码请点击下方代码传送门(飞碟与打靶都在里面)
代码传送门
1、改进飞碟(Hit UFO)游戏:
- 游戏内容要求:
- 按 adapter模式 设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(变换)运动
下面是adapter模式的 UML类图:
题目理解
对于本次游戏要求,我觉着本质上就是一个要求。为啥这么说咧?咱先卖个关子,说下我对于 adapter、proxy 以及 facade 模式的理解。非常欢迎指正······我是真的理解的不太行,太菜了
adapter:适配器模式
什么是适配器模式呢?它的作用是将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
proxy:代理模式
其功能是为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
facade:外观模式
为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
三者区别
先从字面意义上去简单理解:
- 代理模式 就是在客户与类之间加了个中间人,客户要拿到类的数据或者修改都需要先告知中间人,然后中间人去调用类的方法去获取或修改数据。这样,就可以起到限制客户直接访问类的作用,对于数据保护提供了一定安全保障。
- 适配器模式 关键就是 适配 嘛,注重的是 转换与调整。我脑补的例子是有可能有多个不同的类实现了相似的某种功能,但有些许的不同,所以我们需要根据需求去使用不同的类所实现的函数。这样,我们就需要提供一个接口,使得我们只需要根据简单地修改输入参数就能获取多个类的服务,而不必要去实例化某一个类再使用。
- 外观模式 也正如其名,外观。对于多个子系统,可能会提供各种各样不同的接口。如果我们不加以封装,使用起来会很容易弄混。因此,我们要封装各个子系统提供的接口,提供一些统一的功能,如查询、修改、删除等。统一之后,各种子系统的使用就会变得清晰明了。
可能还是有点懵,其实我也有点,尤其是 facade 与 adapter。耐心理解下,或者再搜搜别的博客看看,应该就能有比较好地见解,然后再来指导我呀
回到最开始的话题,为啥我说两个要求实际上是一个呢?根据 adapter 模式的作用来看,我们要按 adapter 模式修改飞碟游戏,能在哪修改?不就是第二个要求所说的:同时支持物理运动与运动学(变换)运动么!!!有没点道理,哈哈
根据自身理解,与UML图不同的实现
上边的UML图是老师提供的 adapter 设计图。其实吧,单看这个图,我感受不到 adapter 模式,反而很像另一个东西:继承与多态。有没有,有没有,是不是有同样的感觉!IActionManager 仅仅只是个接口,而 CCActionManager 与 PhysisActionManager 仅仅只是实现了这个接口,然后在场景控制器中修改接口指向的类即可。这不就是多态么?!
所以我最终决定实现的时候按照我的理解来了,而不是使用老师提供的UML类图。其实思想上差不多,但是我设计的 IActionManager 是一个类,其中包含了两个对象:CCActionManager 与 PhysisActioinManager。由于两个类都是单例模式,因此两个对象指向的都是两个类的单例。然后,IActionManager 再提供一个函数,输入是调用两个对象中实现的 UFOFly 函数所需要的参数,外加一个用于选择模式的参数:isPhysis。当 isPhysis == 0 时,使用 CCActionManager 中的 UFOFly,isPhysis == 1 时,使用 PhysisActionManager 中的 UFOFly 函数。
这样的实现与 UML 图中区别不算大,但我认为这样才比较好地提供了一个所谓的适配。下面是设计说明:
两个类的 UFOFly 明显是不可同时使用的,但我在同一件任务(飞碟游戏)中又需要用到二者,因此设计一个适配器类:IActionManager 用于综合两个类。其中仅有一个函数,也命名为 UFOFly,但该函数与 CCActionManager 和 PhysisActionManager 中的 UFOFly 有一点区别,即:
- IActionManager 中的 UFOFly 多了一个参数:isPhysis 用于判断使用两个类中的哪个 UFOFly 函数。
这样,客户就可以根据自己的需求简单地设置这个参数,从而获得两个相似但又有所不同的服务。
逼逼赖赖这么多,接下来就直接介绍怎么做咯。
代码修改部分的实现
下面介绍会主要介绍此次改进的部分,至于其他部分的详细介绍请看上一篇博客。
博客传送门
Models
Models变化不大,就在原基础上给 IUserAction 加多了一个函数:setFlyMode()。
public interface IUserAction{
// get player's score
int GetScore();
// get round number
int GetRound();
// 新加函数
// set fly mode: 0: 使用运动学(变换); 1: 使用物理运动;
void setFlyMode(int flyMode);
// Game start
void GameStart();
// Game Over
void GameOver();
// Game restart
void Restart();
// Hit The UFO
void Hit(Vector3 pos);
}
Actions
Actions 是此次改动中 比较多且重要的 的部分。下面是改动的部分:
- 新加了 PhysisAction、PhysisActionManager 以及 IActionManager
- CCFlyActionManager 在 Start() 处将游戏对象中 刚体组件 的 isKinematic 设置为 true,使得该物体不受外力作用,位置、角度等只受代码中的如 transform.position 等 transform 属性的设置影响。
- SSAction 中加多了虚函数 FixedUpdate(),因为对刚体的操作需放在 FixedUpdate 中
下面是这些改动的代码:
SSAction
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() { } // 防止用户自己new抽象的対象
public virtual void Start(){ // 申明虚方法,通过重写实现多态
throw new System.NotImplementedException();
}
public virtual void Update(){
throw new System.NotImplementedException();
}
public virtual void FixedUpdate(){
throw new System.NotImplementedException();
}
}
SSAction 仅仅是添加了个虚函数
CCUFOFlyAction
public override void Start(){
// 不做任何事情
// 新加代码
gameobject.GetComponent<Rigidbody>().isKinematic = true;
}
CCUFOFlyAction 将刚体设置为使用运动学,而非实际物理运动(受外力影响)
PhysisAction
public class PhysisAction : SSAction{
public int side;
public float x_speed = 0.0f;
private PhysisAction() { }
public static PhysisAction GetSSAction(int s, float start){
PhysisAction action = ScriptableObject.CreateInstance<PhysisAction>();//让unity创建动作类,确保内存正确回收
action.side = s;
if(action.side==-1) // -1: left side; 1: right side
action.x_speed = start;
else
action.x_speed = -start;
return action;
}
public override void Update(){
}
public override void FixedUpdate(){
if (this.transform.position.y<90 || this.transform.position.z<-15 || this.transform.position.z>15){
// waiting for destroy
this.destroy = true;
this.callback.SSActionEvent(this); //告诉动作管理或动作组合这个动作已完成
}
}
public override void Start(){
gameobject.GetComponent<Rigidbody>().isKinematic = false;
gameobject.GetComponent<Rigidbody>().velocity = x_speed*new Vector3(0,0,1);
}
}
- GetSSAction 与 CCFlyAction 相似
- FixedUpdate 与 CCFlyAction 中 Update 相似,但无需再计算对象位置等,只需要判断是否超出边界
- Start 中需要将刚体设置为物理运动的,即受到外力影响。同时,还需要设置物体初速度,因为之前物体速度是利用position位置来模拟的,现在不能直接修改位置,所以得给予物体初速
PhysisActionManager
public class PhysisActionManager : SSActionManager, ISSActionCallback{
public PhysisAction fly;
public FirstController sceneController;
protected new void Start(){
sceneController = (FirstController)SSDirector.GetInstance().CurrentScenceController;
// sceneController.action_manager = this;
}
public void UFOFly(GameObject UFO, int side, float start_speed){
fly = PhysisAction.GetSSAction(side,start_speed);
this.RunAction(UFO, fly, this);
}
#region ISSActionCallback implementation
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null){
// 不做任何事情
return;
}
#endregion
}
该类与 CCActionManager 相似,仅仅是把 CCFlyAction 换成 PhysisAction
IActionManager
public class IActionManager : MonoBehaviour{
public CCFlyActionManager fly_manager;
public PhysisActionManager physis_manager;
public FirstController sceneController;
public void Start(){
sceneController = (FirstController)SSDirector.GetInstance().CurrentScenceController;
fly_manager = Singleton<CCFlyActionManager>.Instance;
physis_manager = Singleton<PhysisActionManager>.Instance;
}
public void UFOFly(GameObject UFO, int side, float start_speed, int isPhysis){
if(isPhysis == 1){
physis_manager.UFOFly(UFO,side,start_speed);
}
else{
fly_manager.UFOFly(UFO,side,start_speed);
}
}
}
fly_manager 和 physis_manager 为对应类的单例
sceneController 即 FirstController
UFOFly 中只是简单的条件判断,根据 isPhysis 值使用不同的 UFOFly
FirstController
场景控制器修改内容如下:
- 添加了 PhysisActionManager 与 IActionManager,并进行了初始化
- 添加了 isPhysis 变量,用于判断使用 CCFlyActionManager 还是 PhysisActionManager 中的 UFOFly
- SendDisk 改为使用 IActionManager 中的 UFOFly 函数
- 额外实现了 setFlyMode() 函数,用于设置 isPhysis
变量
// 新加变量
private int isPhysis = 0;
public PhysisActionManager paction_manager;
public IActionManager iaction_manager;
初始化
public void Start (){
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
ccaction_manager = gameObject.AddComponent<CCFlyActionManager>() as CCFlyActionManager;
paction_manager = gameObject.AddComponent<PhysisActionManager>() as PhysisActionManager;
iaction_manager = gameObject.AddComponent<IActionManager>() as IActionManager;
disk_factory = gameObject.AddComponent<DiskFactory>();
score_recorder = gameObject.AddComponent<ScoreRecorder>();
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
game_state = 0;
}
SendDisk 修改
private void SendDisk(){
if(disk_queue.Count != 0){
GameObject disk = disk_queue.Dequeue();
disk_send.Add(disk);
disk.SetActive(true);
// 设置飞碟发送前的初始位置
float ran_y = Random.Range(98.0f, 106.0f);
float ran_z = Random.Range(-1f, 1f) < 0 ? -12.6f : 12.6f;
if(ran_z<0)
disk.GetComponent<DiskData>().appear_side = -1;
else
disk.GetComponent<DiskData>().appear_side = 1;
disk.transform.position = new Vector3(0, ran_y, ran_z);
// 向动作管理器发起动作
float speed = Random.Range(13.0f,16.0f);
// 修改部分
iaction_manager.UFOFly(disk,disk.GetComponent<DiskData>().appear_side,speed,isPhysis);
}
// 飞碟飞出视野后进行回收以及玩家扣血等操作
for (int i = 0; i < disk_send.Count; i++){
GameObject d = disk_send[i];
if ((d.transform.position.y < 92 || d.transform.position.z<-13 || d.transform.position.z>13)&& d.gameObject.activeSelf == true){
disk_factory.FreeDisk(d);
disk_send.Remove(d);
user_gui.DecBlood();
}
}
}
setFlyMode
public void setFlyMode(int flyMode){
isPhysis = flyMode;
}
UserGUI
UserGUI 的修改其实很简单,就只是在 游戏开始 以及 游戏结束 界面加多两个按钮,用于选择飞碟的 Fly 模式。下面只放了修改部分的代码,其它代码可在项目中或上一篇博客中看到。
void OnGUI (){
// 设置style
······
if (game_start){
// 用户点击进行射击
······
// 显示回合数、分数
······
// 显示玩家血量
······
// 游戏结束的操作
if (HP <= 0){
······
if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2-40, 200, 50), "运动学(变换)模式")){
action.setFlyMode(0);
}
if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2+40, 200, 50), "物理运动模式")){
action.setFlyMode(1);
}
action.GameOver();
}
}
else{
······
if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2-40, 200, 50), "运动学(变换)模式")){
action.setFlyMode(0);
}
if (GUI.Button(new Rect(Screen.width / 10, Screen.height / 2+40, 200, 50), "物理运动模式")){
action.setFlyMode(1);
}
}
}
实验结果
本次的实验其实难度相对较小,主要是添加了对刚体的编程操作,以及对新模式的学习。刚体和碰撞体在游戏中我认为可以说是最常用的组件,因此,熟悉这些组件各个属性的作用,以及如何使用代码去控制是非常重要的一个学习任务。
好咯,不吹水了,最后的任务:上图!
游戏开始:
游戏中:
游戏结束:
游戏动态效果:
运动学变换模式:
物理运动模式:
以上就是本次实验的动画结果啦,跟上次变化不大,其实就是多了两个按钮,两种运动模式。
2. 打靶游戏(可选作业):
游戏内容要求:
- 靶对象为 5 环,按环计分;
- 箭对象,射中后要插在靶上
- 增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
- 游戏仅一轮,无限 trials;
- 增强要求:添加一个风向和强度标志,提高难度
最终结果
游戏开始:
游戏中:
游戏结束:
游戏效果展示:
游戏设计
我的设计满足了上述要求中的下列部分:
- 靶对象为 5 环,按环计分
- 箭对象,射中后要插在靶上
- 游戏仅一轮
- 增强要求:添加一个风向和强度标志,提高难度
其中,我觉得 无限 trials 的规则略微有些不合理,所以我设计的是只有五支箭。
游戏预制设计
箭矢设计
先创建一个空对象,然后在对象下创建一个圆柱体和一个方块,方块作为箭头,圆柱体作为箭身。其中,空对象要加 Rigidbody 组件,箭头的 collider 要勾选 Is Trigger。
箭身设计
创建一个空对象,在其下创建五个圆柱体。然后每个圆柱体加入一个 Mesh Collider 组件,并且勾选 Convex。我原本想着每个圆柱体只稍微突出一点,使得肉眼看不出这是五个圆柱体,但我不知道为什么最终碰撞时总是只识别到一个环,所以我最后就分的很开,像上图右边那样,很丑。。。
主要编程部分
此次设计中,没有设计动作管理器,因为就只是使用了物理引擎,没有必要专门再去设计。
Models
public interface IUserAction{
void CreateArrow();
void FollowMouse(Vector3 mousePos);
void Shoot(Vector3 mousePos);
int GetScore();
int GetWindDirection();
int GetWindStrength();
bool IsGameOver();
bool isHoldingArrow();
}
主要是IUserAction的设计,包括创造箭矢,箭矢跟踪鼠标、射出箭矢、获取分数、获取风向、获取风力大小、判断游戏是否结束、是否还握着箭矢(协助判断GameOver)。
ArrowFactory
主要有以下四个函数:
- GetArrow():生产箭矢
- IsAllFree():判断是否所有箭矢都中靶或回收,用于协助判断游戏结束时机,否则可能会提前弹出游戏结束界面
- FreeArrow():回收飞出视野的箭矢
- Reset():重置
FirstController
根据需求实现 IUserAction 以及 ISceneController 的接口即可。
ArrowCollider
可以说是最重要的部分之一。主要是实现箭头检测到碰撞后将箭头设置为不可见,并将父对象 Arrow 的 Rigidbody 组件中的 IsKinematic 属性设置为 true,使得箭身能固定在靶子上。
public class ArrowCollider : MonoBehaviour
{
public ScoreRecorder score_recorder;
// Start is called before the first frame update
void Start()
{
score_recorder = Singleton<ScoreRecorder>.Instance;
}
// Update is called once per frame
void Update()
{
}
void OnTriggerEnter(Collider c){
if(c.gameObject.tag == "target"){
gameObject.transform.parent.gameObject.GetComponent<Rigidbody>().isKinematic = true;
gameObject.SetActive(false); //这样设置是为了防止多次触发
int point = c.gameObject.name[c.gameObject.name.Length - 1] - '0';
score_recorder.Record(point);
}
}
}
具体效果请到 github 中下载来试下咯。地址在顶部。