3D游戏编程与设计 HW 6 物理系统与碰撞
程序员文章站
2022-07-13 08:35:06
...
3D游戏编程与设计 HW 6物理系统与碰撞
Hit UFO v2.0 完整游戏过程可见以下视频:
https://www.bilibili.com/video/BV19p4y1r7MJ/
Hit UFO v2.0 完整代码可见以下仓库:
https://gitee.com/beilineili/game3-d
1.改进飞碟(Hit UFO v2.0)游戏:
①游戏内容要求
- 按 adapter模式 设计图修改飞碟游戏
- 使它同时支持物理运动与运动学(变换)运动
2.游戏设计
① 游戏玩法
- 通过用鼠标点击飞出来的UFO来得分,其中绿色飞碟为1分,黄色飞碟为2分,红色飞碟为3分
- 分为四个游戏模式,分为简单,普通,困难和无限模式,其中开始时游戏默认是 Normal 模式。
- 除了无限模式有无限轮次的飞碟外,其他模式都是有10轮,所有飞碟发射完毕时游戏结束
② 与 Hit UFO v1.0 版本的不同
Hit UFO v1.0(基础版)完整代码:
https://gitee.com/beilineili/game3-d/tree/master/4.1.Hit_UFO
- 这次的改动建立在之前的 Hit UFO 基础上,在 Hit UFO v2.0 中,我们不删除原本的正常运动模式,而是让其与新的物理运动模式共存,在游戏运行的时候来决定使用哪种。即,原本的动作管理器类不删除,它们是管理正常运动模式的。我们再实现另一个用来管理物理运动模式的动作管理器。最后将它们结合起来
- 物理引擎将游戏世界对象赋予现实世界的物理属性,并抽象为刚体模型,使得游戏物体在力的作用下,仿真现实世界的运动及其之间的碰撞过程。
- 关于物理引擎的动作改动肯定离不开对动作类的更改,这里我们引入 Adapter 模式
Adapter Pattern(适配器模式)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。我们希望 FirstController 只需为同一个用途的所有组件保存1个变量,要将两个动作管理器同时接入 FirstController,就要实现一个适配器,让 FirstController 连接适配器,然后让适配器分别连接两个动作管理器。
举个例子,当电源插头不够用时,我们会买一个插座来让更多的设备可以用上电。
③ 代码设计
- 由于有很多代码是复用的,下面只给出更改和新增的代码文件,其余部分请浏览上一篇博客 3D游戏编程与设计 HW 5 与游戏世界交互
一、Controller
1)修改 FlyAction.cs
- 原先的 Start 函数是空实现,在这里因为 FlyAction 是运动学的飞行,所以生成对象时要将刚体设为运动学的,即不会碰撞检测
- 将飞行运动拆分成水平和竖直两个方向,其中水平方向速度恒定,竖直方向添加重力加速度
- 当飞碟到达底部时,即飞碟的高度在摄像机观察范围之下时,动作结束,将进行回调
public class FlyAction : SSAction
{
float gravity; //重力加速度
float speed; //初始速度
float time; //时间
Vector3 direction; //初始飞行方向
public static FlyAction GetSSAction(Vector3 direction, float speed)
{
FlyAction action = ScriptableObject.CreateInstance<FlyAction>();
action.gravity = 9.8f;
action.time = 0;
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
gameObject.GetComponent<Rigidbody>().isKinematic = true;
}
public override void Update()
{
time += Time.deltaTime;
//竖直方向上有重力加速度
transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
//初始飞行方向匀速运动
transform.Translate(direction * speed * Time.deltaTime);
//飞碟到达画面底部,动作结束,进行回调
if (this.transform.position.y < -6)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
2)新增 IActionManager.cs
- 动作管理类的接口,基于这个接口来实现动作管理类的 Adapter 模式
public interface IActionManager
{
void Fly(GameObject disk, float speed, Vector3 direction);
}
3) 修改 ActionManager.cs
- 飞行动作管理类,用于生成飞碟飞行动作,和接受飞行动作的回调信息,回收飞碟
- ActionManager 现在需要实现 IActionManager 的接口
public class ActionManager : SSActionManager, ISSActionCallback, IActionManager
{
//飞行动作
public FlyAction flyAction;
//控制器
public FirstController controller;
protected new void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
controller.actionManager = this;
}
public void Fly(GameObject disk, float speed, Vector3 direction)
{
flyAction = FlyAction.GetSSAction(direction, speed);
RunAction(disk, flyAction, this);
}
//回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
//飞碟结束飞行后工厂进行回收
controller.diskFactory.FreeDisk(source.gameObject);
}
}
4) 修改 IUserAction.cs
- 用户动作接口,有点击,重新开始和选择游戏模式三个函数的接口
- 现在 IUserAction 提供了设置飞行模式的接口
public interface IUserAction
{
void Hit(Vector3 position); //点击
void Restart(); //重新开始
void SetMode(int mode); //选择游戏模式
void SetFlyMode(int flyMode); //设置飞碟运动模式
}
5)修改 FirstController.cs
- 场景控制器,负责游戏逻辑
- 发送飞碟 SendDisk – 从工厂获得一个飞碟并设置初始位置和速度,飞行动作
- 点击判断 Hit – 处理用户点击动作,将被点击到的飞碟回收,计算得分
在游戏中,玩家通过鼠标点击飞碟,从而得分。这当中涉及到一个点击判断的问题。我们调用 ScreenPointToRay 方法,构造由摄像头和屏幕点击点确定的射线,与射线碰撞的游戏对象即为玩家点击的对象
- 为了降低 FirstController 代码的复杂度,我们分解它的部分功能到新增的 RoundConrtoller 中实现
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public ActionManager actionManager;
public DiskFactory diskFactory; //飞碟工厂
RoundController roundController;
UserGUI userGUI;
void Start()
{
LoadResources();
}
public void LoadResources()
{
SSDirector.GetInstance().CurrentScenceController = this;
gameObject.AddComponent<DiskFactory>();
gameObject.AddComponent<ActionManager>();
gameObject.AddComponent<PhysisActionManager>();
gameObject.AddComponent<RoundController>();
gameObject.AddComponent<UserGUI>();
diskFactory = Singleton<DiskFactory>.Instance;
roundController = Singleton<RoundController>.Instance;
userGUI = Singleton<UserGUI>.Instance;
}
//处理用户的点击动作
public void Hit(Vector3 position)
{
Camera ca = Camera.main;
Ray ray = ca.ScreenPointToRay(position);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
//如果用户点击到飞碟
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
//将飞碟移至底部,触发飞行动作的回调
hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
//分数增加
roundController.Record(hit.collider.gameObject.GetComponent<DiskData>());
//更新GUI
userGUI.SetPoints(roundController.GetPoints());
}
}
}
//重新开始
public void Restart()
{
userGUI.SetResult("");
userGUI.SetPoints(0);
userGUI.SetRound(0);
userGUI.SetMode(1);
userGUI.SetFlyMode(0);
roundController.Reset();
}
//设置模式
public void SetMode(int mode)
{
roundController.SetMode(mode);
}
public void SetFlyMode(int flyMode){
roundController.SetFlyMode(flyMode);
}
public void FreeDisk(GameObject disk){
diskFactory.FreeDisk(disk);
}
void Update()
{
}
}
6)新增 PhysisFlyAction.cs
- 实现物体的物理学飞行
- 当不使用物理引擎时启用 isKinematic 运动学变换,当使用物理引擎时关闭运动学变换
- 给飞碟预制体添加刚体属性,选择Use Gravity,最后增加一个初速度,飞碟就会自动实现重力和碰撞效果
- 利用的是物体的刚体属性,以及物理引擎实现物理运动
public class PhysisFlyAction : SSAction
{
float speed; //初始速度
Vector3 direction; //飞行初始方向
//生产函数
public static PhysisFlyAction GetSSAction(Vector3 direction, float speed)
{
PhysisFlyAction action = ScriptableObject.CreateInstance<PhysisFlyAction>();
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
//设置刚体属性
gameObject.GetComponent<Rigidbody>().isKinematic = false;
gameObject.GetComponent<Rigidbody>().velocity = speed * direction;
}
public override void Update()
{
//如果飞碟到达底部,则动作结束,进行回调
if (this.transform.position.y < -6)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
7)新增 PhysisActionManager.cs
- 物理模式的动作管理类,代码与 ActionManager 类似
public class PhysisActionManager : SSActionManager, ISSActionCallback, IActionManager
{
PhysisFlyAction flyAction;
FirstController controller;
protected new void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
}
public void Fly(GameObject disk, float speed, Vector3 direction)
{
flyAction = PhysisFlyAction.GetSSAction(direction, speed);
RunAction(disk, flyAction, this);
}
//回调函数,飞碟结束飞行后进行回收
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
controller.FreeDisk(source.gameObject);
}
}
8)新增 RoundController.cs
- RoundController 实现的是 Hit UFO 1.0 中 FirstController 每回合发送飞碟、计分、计轮次等功能的代码
public class RoundController : MonoBehaviour
{
FirstController controller;
IActionManager actionManager; //动作管理者
DiskFactory diskFactory; //飞碟工厂
UserGUI userGUI;
int[] roundDisks; //对应轮次的飞碟数量
int mode; //当前模式,简单-0,正常-1,困难-2,或者无限模式-3
int points; //当前分数
int round; //当前轮次
int sendCount; //当前已发送的飞碟数量
float sendTime; //发送时间
void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
actionManager = Singleton<ActionManager>.Instance;
diskFactory = Singleton<DiskFactory>.Instance;
userGUI = Singleton<UserGUI>.Instance;
roundDisks = new int[] { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };
mode = 1;
points = 0;
round = 1;
sendCount = 0;
sendTime = 0;
}
public void Reset()
{
mode = 1;
points = 0;
round = 1;
sendCount = 0;
sendTime = 0;
}
public void Record(DiskData disk)
{
points += disk.points;
}
public int GetPoints()
{
return points;
}
public void SetMode(int mode)
{
this.mode = mode;
}
public void SetFlyMode(int flyMode)
{
actionManager = flyMode == 1 ? Singleton<PhysisActionManager>.Instance : Singleton<ActionManager>.Instance as IActionManager;
}
public void SendDisk(int mode)
{
//从工厂生成一个飞碟
GameObject disk = diskFactory.GetDisk(round);
//设置飞碟的随机位置
disk.transform.position = new Vector3(-disk.GetComponent<DiskData>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
disk.SetActive(true);
//设置飞碟的飞行动作
if (mode == 0){
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*0.5f, disk.GetComponent<DiskData>().direction);
}
else if (mode == 2) {
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed*1.5f, disk.GetComponent<DiskData>().direction);
}
else {
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);
}
}
// Update is called once per frame
void Update()
{
sendTime += Time.deltaTime;
//每隔1s发送一次飞碟
if (sendTime > 1)
{
sendTime = 0;
//每次发送至多5个飞碟
for (int i = 0; i < 5 && sendCount < roundDisks[round-1]; i++)
{
sendCount++;
SendDisk(mode);
}
//判断是否需要重置轮次,不需要则输出游戏结束
if (sendCount == roundDisks[round-1] && round == roundDisks.Length)
{
if (mode == 3)
{
round = 1;
sendCount = 0;
userGUI.SetResult("");
}
else
{
userGUI.SetResult("Game Over!");
}
}
//更新轮次
if (sendCount == roundDisks[round-1] && round < roundDisks.Length)
{
sendCount = 0;
round++;
userGUI.SetRound(round);
}
}
}
}
二、Model
- Model 的三个代码文件都不需要更改,直接复用 Hit UFO 基础版的代码
三、 View
1)修改 UserGUI.cs
- 界面类,用于构建 UI 和捕捉用户动作,将分数,Round,游戏模式显示出来
- 新增两个设置运动模式的按钮,且将UserGUI的成员设置成了私有
public class UserGUI : MonoBehaviour
{
IUserAction userAction;
string result;
int points;
int round;
int mode;
int flyMode;
public void SetResult(string result){
this.result = result;
}
public void SetPoints(int points){
this.points = points;
}
public void SetRound(int round){
this.round = round;
}
public void SetMode(int mode){
this.mode = mode;
}
public void SetFlyMode(int flyMode){
this.flyMode = flyMode;
}
void Start()
{
result = "";
points = 0;
round = 1;
mode = 1;
flyMode = 0;
userAction = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
//打印和用户交互提示界面
void OnGUI()
{
GUIStyle titleStyle = new GUIStyle();
titleStyle.normal.textColor = Color.black;
titleStyle.fontSize = 50;
GUIStyle style = new GUIStyle();
style.normal.textColor = Color.white;
style.fontSize = 30;
GUIStyle resultStyle = new GUIStyle();
resultStyle.normal.textColor = Color.red;
resultStyle.fontSize = 50;
GUIStyle modeStyle = new GUIStyle();
modeStyle.normal.textColor = Color.blue;
modeStyle.fontSize = 40;
GUI.Label(new Rect(600, 30, 50, 200), "Hit UFO", titleStyle);
GUI.Label(new Rect(20, 10, 100, 50), "Points: " + points, style);
GUI.Label(new Rect(220, 10, 100, 50), "Round: " + round, style);
GUI.Label(new Rect(1000, 100, 50, 200), result, resultStyle);
if (GUI.Button(new Rect(1300, 50, 100, 50), "Restart"))
{
userAction.Restart();
}
//简单模式
if (GUI.Button(new Rect(1300, 125, 100, 50), "Easy Mode"))
{
userAction.SetMode(0);
}
//通常模式
if (GUI.Button(new Rect(1300, 200, 100, 50), "Normal Mode"))
{
userAction.SetMode(1);
}
//困难模式
if (GUI.Button(new Rect(1300, 275, 100, 50), "Hard Mode"))
{
userAction.SetMode(2);
}
//无限模式
if (GUI.Button(new Rect(1300, 350, 100, 50), "Infinite Mode"))
{
userAction.SetMode(3);
}
//运动学模式
if (GUI.Button(new Rect(1300, 425, 100, 50), "Kinematics"))
{
userAction.SetFlyMode(0);
}
//物理模式
if (GUI.Button(new Rect(1300, 500, 100, 50), "Physis"))
{
userAction.SetFlyMode(1);
}
//捕捉鼠标点击
if (Input.GetButtonDown("Fire1"))
{
userAction.Hit(Input.mousePosition);
}
}
}
3. 游戏界面
- 调整了飞碟预制的形状和大小,让它相比之前看起来更像是飞碟
- 物理运动模式下游戏画面,飞碟之间会互相碰撞
上一篇: 如何用Python写贪吃蛇小游戏