概述
文章目录
- 写在前面
- HitUFO的物理引擎改进版本
- 物理引擎的改进版本思路与实现
- PhysicsAction
- PhysicsManager
- 新接口类IActionManager
- 动作管理器基类的变化
- Controller变化
- UI变化
- 一点小细节
- 游戏效果:
- 射箭游戏设计与实现
- 游戏要求:
- 具体实现代码
- 动作部分
- 碰撞检测
- 工厂类生产箭
- Controller类
- UI类
- 游戏效果
写在前面
- Unity3d学习制作的实验资料都在老师的课程网站上:传送门
- 本次实验的项目有两部分,打飞碟的物理引擎改进以及射箭游戏的设计。项目文件在Github上:
- 打飞碟
- 射箭游戏
- 视频链接:
- 打飞碟(与上一个项目的演示没什么太大区别)
- 射箭游戏
- 此次打飞碟游戏的改进是在上一次实验的基础上进行的(上一次的博客地址)
HitUFO的物理引擎改进版本
回看内容要求:
游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定
自己设定的一些规则:
- 本次游戏设置了3个round,虽然round比较少,但是难度递增明显,而且有一定运气成分,也就是说即使到了最后一个round,也有可能出现比较简单的trial。
- 游戏难度主要由飞碟飞行速度(不同颜色代表不同属性),飞碟同时出现个数决定,大致由round控制,但是带有随机性。
- 当一个trial内的飞碟全部坠毁后才切换到下一个trial,两个trial之间间隔不小于1.5秒。
- 飞碟得分分别为1分、2分、3分对应3种速度的飞碟。
物理引擎的改进版本思路与实现
由于游戏逻辑和界面都没有必要改变,物理引擎的引用就是关于飞碟运动的部分,所以仅仅需要对Action相关的类进行改进即可。
在原来的类的基础上,加上利用物理引擎的组件RigidBody进行物体的运动。
RigidBody主要用到以下功能:
- 自动添加重力,也就是勾选的Gravity(默认),也就是说不必思考如何向下加速的运动
- AddForce的函数,给物体施加力。这里的目的主要是给物体一个初速度让其模拟飞碟被抛出的运动,所以这个力不必持续施加,只需在飞出的时候添加一段极小时间即可。所以选择使用ForceMode.Impulse这个模式,瞬间力,符合我们的目的。
- 至于物体碰撞旋转之类的,可以通过冻结某个轴的旋转来使飞碟更加稳定。
好,那么接下来,我们需要添加一个物理运动的类,基类还是基本的SSAction,只需要继承并实现多态即可。
PhysicsAction
直接上代码:
public class PhysicsAction : SSAction {
public Vector3 forces;
private bool once = true;
public static PhysicsAction GetSSAction(Vector3 target, float speed) {
PhysicsAction action = ScriptableObject.CreateInstance<PhysicsAction>();
action.forces = target * speed;
return action;
}
public override void FixedUpdate() {
if (once) {
this.gameObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
this.gameObject.GetComponent<Rigidbody>().AddForce(forces, ForceMode.Impulse);
// Debug.Log("Forces!");
once = false;
}
if (this.transform.position.y <= -5) {
this.destroy = true;
if (this.transform.position.y > -15) {
Singleton<Judger>.Instance.Miss();
}
this.callBack.SSActionEvent(this);
Controller c = Director.getInstance().currentSceneController as Controller;
c.used --;
}
}
public override void Update(){
}
public override void Start() {
}
}
为了代码能够服用,函数传入参数不做改变,但是内在逻辑改变,也就是说行为变化,但是接口还是同一个,实现多态。由于在运动学实现中,需要目标方向,以及一个速度,而物理引擎中,是通过添加力的方式来实现运动,所以这里简单将目标方向target和speed相乘,表示力的方向和大小。然后返回action
在Update里,由于力不能持续施加,所以需要判断是否第一次施加。施加了之后物体就有一个初速度,然后随着重力的作用,做抛物线运动。十分简单。
PhysicsManager
public class PhysicsManager : ActionManager, ISSActionCallback, IActionManager {
PhysicsAction action;
Controller controller;
private void Start()
{
controller = Director.getInstance().currentSceneController as Controller;
controller.actionManager = this;
}
public void flyUFO(GameObject disk, Vector3 target, float speed) {
action = PhysicsAction.GetSSAction(target, speed);
if (disk.GetComponent<Rigidbody>() == null) {
disk.AddComponent<Rigidbody>();
disk.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeRotationX|RigidbodyConstraints.FreezeRotationZ;
}
this.RunAction(disk, action, this);
}
public void SSActionEvent(SSAction action){
Singleton<DiskFactory>.Instance.freeDisk(action.gameObject);
}
}
动作管理器也类似,保持函数接口不变,直接执行RunAction就可以了,但是有一点要注意的是,由于飞碟工厂在创建的时候并不知道是否采用物理运动,所以不会飞碟实例添加刚体,需要在动作管理器实现给物体添加刚体,注意飞碟会复用,不能重复添加!
新接口类IActionManager
由于两个类(物理运动和运动学)都有同样的函数,不同的实现,而在Controller里面调用的时候,需要一个统一的接口所以新建一个接口:
public interface IActionManager {
void flyUFO(GameObject disk, Vector3 target, float speed);
}
动作管理器基类的变化
由于动力学和物理运动使用的更新帧函数是不同的(一个FixedUpdate,一个Update)所以在管理器中也应当实现两个Update函数,分别执行动作类的两个Update函数。
代码如下:
public class ActionManager : MonoBehaviour{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingForAdd = new List<SSAction>();
private List<int> waitingForDelete = new List<int>();
private Controller controller;
private void Start()
{
}
protected void FixedUpdate()
{
foreach (SSAction action in waitingForAdd)
{
actions[action.GetInstanceID()] = action;
}
waitingForAdd.Clear();
foreach (KeyValuePair<int,SSAction> pair in actions)
{
SSAction action = pair.Value;
if (action.destroy)
{
waitingForDelete.Add(action.GetInstanceID());
} else if (action.enable)
{
action.FixedUpdate();
}
}
foreach (int key in waitingForDelete)
{
SSAction action = actions[key];
actions.Remove(key);
Destroy(action);
}
waitingForDelete.Clear();
}
protected void Update()
{
foreach (SSAction action in waitingForAdd)
{
actions[action.GetInstanceID()] = action;
}
waitingForAdd.Clear();
foreach (KeyValuePair<int,SSAction> pair in actions)
{
SSAction action = pair.Value;
if (action.destroy)
{
waitingForDelete.Add(action.GetInstanceID());
} else if (action.enable)
{
action.Update();
}
}
foreach (int key in waitingForDelete)
{
SSAction action = actions[key];
actions.Remove(key);
Destroy(action);
}
waitingForDelete.Clear();
}
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback callback)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callBack = callback;
waitingForAdd.Add(action);
action.Start();
}
}
Controller变化
由于需要选择运动的模式,所以需要另外创建一个枚举变量,设置不同的状态,并且给控制器添加相应的动作管理器:
public void setMode(ActionMode m) {
mode = m;
if (mode == ActionMode.PHYSICS) {
if (gameObject.GetComponent<PhysicsManager>() == null)
actionManager = gameObject.AddComponent<PhysicsManager>() as PhysicsManager;
else
actionManager = gameObject.GetComponent<PhysicsManager>() as PhysicsManager;
}
else if (mode == ActionMode.MOVE) {
if (gameObject.GetComponent<FlyActionManager>() == null)
actionManager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
else
actionManager = gameObject.GetComponent<FlyActionManager>() as FlyActionManager;
}
}
UI变化
在UI上设置相应的按钮,在游戏开始的时候设置相应的Mode就可以了。相当于将原本的Play按钮分裂成两个Mode选择按钮:
if (flag) {
GUI.Label(new Rect(Screen.width/2-50, Screen.height/2-95, 100, 50), "Hit UFO!", style2);
if (GUI.Button(new Rect(Screen.width/2-180, Screen.height/2 -30, 180, 70), "Physisc Mode", style1)) {
action.setMode(ActionMode.PHYSICS);
flag = false;
action.changeState(1);
}
if (GUI.Button(new Rect(Screen.width/2+60, Screen.height/2 -30, 190, 70), "Dynamics Mode", style1)) {
action.setMode(ActionMode.MOVE);
flag = false;
action.changeState(1);
}
// 注释部分为原Play按钮
// if(GUI.Button(new Rect(Screen.width/2-70, Screen.height/2 - 20, 150, 70), "Play", style1)) {
// if (!mode) {
// flag = false;
// action.changeState(1);
// }
// }
}
开始界面UI图:
一点小细节
由于动作管理器接口是一定的,就是传入参数也是一定的,只有target和speed
public interface IActionManager {
void flyUFO(GameObject disk, Vector3 target, float speed);
}
但是对于两个不同的模式,同样参数得到的运动效果是不同的,就可以通过一点小小的公式变化(自己构造)来实现,使得两个模式下的飞碟能够正常演示。这里我设置得有点小失误,所以运动学的模式下会显得飞碟慢一点,减小了游戏难度,不过对于本次实验学习来说,还算可以。
游戏效果:
与之前版本相差不大,但是添加了刚体之后会出现飞碟碰撞的情况,略微增加了游戏难度:(比如说两个飞碟碰撞之后速度和方向会相应变化,此处还有一点小bug就是如果两个飞碟的起始位置十分靠近,就会在飞出的瞬间弹开,这个可以通过随机计算位置的时候增加一点约束来实现)
射箭游戏设计与实现
游戏要求:
游戏内容要求:
靶对象为 5 环,按环计分;
箭对象,射中后要插在靶上
增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
游戏仅一轮,无限 trials;
增强要求:添加一个风向和强度标志,提高难度
具体实现思路:
- 设计扁平圆柱体作为靶标,多个不同大小的圆柱体叠加形成不同的环,并利用颜色区分,由于叠加会影响正常显示,所以每个圆柱体的宽度(高)需要不一致,或者利用位置不同实现一个层级的效果,小的在前,大的在后就能显示出一个个环的效果。
- 由于对游戏轮次没有太大的要求,所以这里设置为游戏开始时拥有一定数量的箭,箭用完就算结束,显示得分,由用户决定是否再来一次(对于无限trails的规则不太清楚)。
- 风向则可是使用一个持续添加在箭上的力来实现。
具体实现代码
首先用到之前几个游戏的一些基类:Director、SSAction、SSActionManager、Singleton等,这几个类由于只是基类,真正的逻辑实现都在子类中,所以直接重用。
动作部分
先来说说游戏的动作部分,首先分析主要的运动对象:箭。。。没了。所以与之前打飞碟的游戏类似,只需要实现将箭飞出去的动作,通过物理引擎的实现之前也提到过,只需要在出去的瞬间给它添加一个力就可以了。
其次是风力的作用,与飞出去的瞬间力不同,风力是需要持续作用的力,所以需要在FixedUpdate里持续添加。
从以上可以看出,此动作类主要的关键属性有:运动方向、风力作用方向。
ArrowAction
代码如下:
public class ArrowAction : SSAction{
public Vector3 force;
public Vector3 affect;
public static ArrowAction GetSSAction(Vector3 f, Vector3 wind) {
ArrowAction action = ScriptableObject.CreateInstance<ArrowAction>();
action.force = f;
action.affect = wind;
return action;
}
public override void FixedUpdate() {
this.gameObject.GetComponent<Rigidbody>().AddForce(affect, ForceMode.Acceleration);
if (this.transform.position.z > 3 || Mathf.Abs(this.transform.position.y) > 7 ||
Mathf.Abs(this.transform.position.x) > 10 || this.gameObject.tag == "ontarget") {
this.destroy = true;
if (this.gameObject.tag != "ontarget")
this.callBack.SSActionEvent(this);
}
}
public override void Update(){}
public override void Start() {
this.gameObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
this.gameObject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
}
}
所以构造此类的时候需要的参数也就是两个,分别对应两个关键属性。
此外,当箭超出一定范围时,我们就可以认为它无法到达靶标,直接中止动作执行。或者当其到达目标时,(名字会设置为ontarget)也可以中止动作。
动作管理器类也就是简单的包装一下动作类,并且使其执行,并且实现回调函数,这里的回调就是用弓箭工厂的方法将箭的对象释放掉(加入到free的队列去,以便重新使用):
ArrowActionManager
public class ArrowActionManager : SSActionManager, ISSActionCallback {
ArrowAction arrowAction;
Controller controller;
private void Start()
{
controller = Director.getInstance().currentSceneController as Controller;
controller.actionManager = this;
}
public void arrowFly(GameObject arrow, Vector3 target, Vector3 wind) {
arrowAction = ArrowAction.GetSSAction(target, wind);
if (arrow.GetComponent<Rigidbody>() == null)
arrow.AddComponent<Rigidbody>();
else
arrow.GetComponent<Rigidbody>().isKinematic = false;
this.RunAction(arrow, arrowAction, this);
}
public void SSActionEvent(SSAction action){
Singleton<ArrowFactory>.Instance.freeArrow(action.gameObject);
if (controller.arrow.name == "arrow")
controller.getArrow();
}
}
值得注意的一点是,由于箭未发出的时候,是需要在弓上停留的,也即是说不能有刚体的重力作用,否则就会掉下去。所以在运动管理器中,需要在执行射箭动作之前,恢复刚体的作用,这里采用的是运动学模式和物理模式切换的方式来实现。如果是新创建的实例,还没有添加刚体部件则需要添加。
回调函数里主要是运动结束后执行的行为,运动结束主要有两个状态,上靶和未中靶,上靶的箭不能free掉,因为要保留来积分(模拟真实场景),所以还需要额外判断当前的箭是脱靶了还是中靶的。
碰撞检测
主要是利用碰撞体的Trigger,来检测是否碰撞到了,如果碰到了就积分,不同的碰撞体不同分,然后将箭的名字(用来代表状态)改成ontarget,这样就防止别的碰撞器也重复积分。因为会实际上两个碰撞器的距离很微小,所以能够同时碰到多个碰撞器。
public class CollisionRev : MonoBehaviour {
private void OnTriggerEnter(Collider other)
{
GameObject arrow = other.gameObject;
if (arrow.name == "arrow") {
string str = this.name;
arrow.GetComponent<Rigidbody>().velocity = Vector3.zero;
arrow.GetComponent<Rigidbody>().isKinematic = true;
Singleton<Judger>.Instance.addScore(str);
arrow.transform.position += Vector3.forward * 0.001f;
arrow.name = "ontarget";
Controller controller = Director.getInstance().currentSceneController as Controller;
controller.hit(arrow);
// if (controller.arrow == null)
controller.getArrow();
}
}
}
工厂类生产箭
与前一个实验的飞碟工厂很像,而且不需要添加属性什么的,更加简单。
只需要将空闲的或者刚创建的箭,初始化位置等就可以了。还有一个Free的方法,也是跟之前类似。
public class ArrowFactory : MonoBehaviour {
public GameObject arrow = null;
private List<GameObject> activeList = new List<GameObject>();
private List<GameObject> freeList = new List<GameObject>();
public GameObject getArrow() {
if (freeList.Count > 0) {
arrow = freeList[0].gameObject;
freeList.Remove(freeList[0]);
arrow.GetComponent<Rigidbody>().isKinematic = true;
}
else {
arrow = Instantiate(Resources.Load("Prefabs/arrow", typeof(GameObject))) as GameObject;
}
arrow.transform.rotation = Quaternion.Euler(0,0,0);
arrow.transform.position = new Vector3(-0.1f, 0.85f, -9.7f);
arrow.SetActive(true);
arrow.name = "ready";
activeList.Add(arrow);
return arrow;
}
public void freeArrow(GameObject a) {
for (int i = 0; i < activeList.Count; i ++) {
if (a.GetInstanceID() == activeList[i].gameObject.GetInstanceID()) {
activeList[i].gameObject.SetActive(false);
freeList.Add(activeList[i]);
activeList.Remove(activeList[i]);
break;
}
}
}
}
Controller类
public class Controller : MonoBehaviour, SceneController, Interaction
{
public ArrowActionManager actionManager;
public ArrowFactory factory;
public GameObject bow;
public GameObject target;
public GameObject arrow;
public Judger judger;
public Vector3 direction;
public UI ui;
public int state = 0;
private int arrowNumber = 0;
private Queue<GameObject> hit_arrow = new Queue<GameObject>();
public Vector3 wind = Vector3.zero;
private int[] direc = {1,-1,0};
private void Start() {
Director director = Director.getInstance();
director.currentSceneController = this;
factory = this.gameObject.AddComponent<ArrowFactory>();
actionManager = this.gameObject.AddComponent<ArrowActionManager>() as ArrowActionManager;
ui = this.gameObject.AddComponent<UI>();
judger = this.gameObject.AddComponent<Judger>();
// factory = Singleton<ArrowFactory>.Instance;
loadResources();
int x = Random.Range(0,3);
int y = Random.Range(0,3);
x = direc[x];
y = direc[y];
int level = Random.Range(1,5);
wind = new Vector3(x, y, 0) * level;
}
public void loadResources() {
bow = Instantiate(Resources.Load("Prefabs/bow", typeof(GameObject))) as GameObject;
target = Instantiate(Resources.Load("Prefabs/target", typeof(GameObject))) as GameObject;
arrow = factory.getArrow();
}
private void Update()
{
}
public void moveArrowDirection(Vector3 to) {
if (state <= 0) {
return;
}
arrow.transform.rotation = Quaternion.LookRotation(to);
bow.transform.rotation = Quaternion.LookRotation(to);
direction = to;
}
public void reuse() {
int tmp = hit_arrow.Count;
for (int i = 0; i < tmp; i ++) {
factory.freeArrow(hit_arrow.Dequeue());
}
arrowNumber = 0;
}
public void shoot(Vector3 force) {
if (state > 0 && arrow != null) {
// arrow = factory.getArrow();
arrow.name = "arrow";
actionManager.arrowFly(arrow, direction * 15, wind);
arrowNumber ++;
}
}
public void hit(GameObject arrow) {
hit_arrow.Enqueue(arrow);
}
public void getArrow() {
int x = Random.Range(0,3);
int y = Random.Range(0,3);
x = direc[x];
y = direc[y];
int level = Random.Range(1,5);
wind = new Vector3(x, y, 0) * level;
if (state == 1) {
if (arrowNumber > 7) {
setState(-1);
}
}
arrow = factory.getArrow();
}
public void setState(int s) {
state = s;
}
public void restart() {
state = 0;
arrowNumber = 0;
}
public int getState() {
return state;
}
public string arrowState() {
return arrow != null ? arrow.name : null;
}
public Vector3 getWind() {
return wind;
}
}
这里面主要是实现了射箭、获取箭的函数,还有一部分与用户交互的函数。
loadResources
就是加载弓箭和靶子的资源。
射箭shoot
主要是状态的改变,之前已经说过,用名字来代表状态,在射出的时候更改状态为arrow表示在飞行中。
moveDirection
函数主要是更改弓箭的朝向,这里是与鼠标的位置相关,也就是利用鼠标更改朝向,使得其能够瞄准。
getArrow
则是获取弓箭,也就是下一次射箭的准备工作,需要把风向提前设置好,这里使用随机的方式生成8个方向的风,风力等级也是随机,有4个等级。
hit
则是类似回调,将中箭的加入使用中的队列,因为这部分箭不会自动收回(只收回了脱靶的)
还有一些获取状态的函数,游戏状态、风力信息、弓箭状态等。
UI类
UI主要是负责用户的交互,所以需要获取Controller的一些状态来判断用户操作是否合法或者限制用户的操作。
public class UI : MonoBehaviour {
Interaction interaction;
bool flag = true;
GUIStyle style1;
GUIStyle style2;
GUIStyle style3;
float time = 0;
private void Start() {
interaction = Director.getInstance().currentSceneController as Interaction;
style1 = new GUIStyle("button");
style1.fontSize = 25;
style2 = new GUIStyle();
style2.fontSize = 35;
style2.alignment = TextAnchor.MiddleLeft;
style3 = new GUIStyle();
style3.fontSize = 15;
style3.alignment = TextAnchor.MiddleLeft;
}
private void OnGUI()
{
if (interaction.getState() == -1) {
if (time < 2) {
time += Time.deltaTime;
GUI.Label(new Rect(Screen.width/2-70, Screen.height/2-135, 200, 30), "Preparing Arrow...", style2);
} else {
GUI.Label(new Rect(Screen.width/2-70, Screen.height/2-135, 200, 30), "Your Score:" + Singleton<Judger>.Instance.getScore().ToString(), style2);
if (GUI.Button(new Rect(Screen.width/2-70, Screen.height/2 - 20, 180, 70), "Play again", style1)) {
interaction.reuse();
interaction.setState(1);
Singleton<Judger>.Instance.restart();
time = 0;
}
}
}
GUI.Label(new Rect(5, 5, 100, 30), "Score: " + Singleton<Judger>.Instance.getScore().ToString(), style3);
Vector3 wind = interaction.getWind();
int x = (int)wind.x;
int y = (int)wind.y;
string str1, str2, level;
if (x < 0)
str1 = "West";
else if (x > 0)
str1 = "East";
else
str1 = "";
if (y < 0)
str2 = "South";
else if (y > 0)
str2 = "North";
else
str2 = "";
if (x == 0 && y == 0) {
str1 = "No wind";
}
if (x != 0) {
int tmp = x > 0 ? x : -x;
level = tmp.ToString();
} else if (y != 0) {
int tmp = y > 0 ? y : -y;
level = tmp.ToString();
} else {
level = "0";
}
GUI.Label(new Rect(5, 35, 200, 30), "Wind Direction: " + str1 + str2, style3);
GUI.Label(new Rect(5, 65, 100, 30), "Wind Level: " + level , style3);
if (flag) {
GUI.Label(new Rect(Screen.width/2-60, Screen.height/2-135, 100, 50), "ShootArrow!", style2);
if(GUI.Button(new Rect(Screen.width/2-70, Screen.height/2 - 20, 150, 70), "Play", style1)) {
flag = false;
interaction.setState(1);
}
}
}
private void Update()
{
if (interaction.getState() > 0) {
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (interaction.arrowState() == "ready") {
interaction.moveArrowDirection(ray.direction);
if (Input.GetButtonDown("Fire1")) {
interaction.shoot(ray.direction);
}
}
}
}
}
这里主要是根据Controller的状态(游戏未开始、进行中、结束)来显示不同的界面,如果是进行中,还需要显示分数、风力信息等,这里是通过Controller的风力向量,临时计算风力信息,有点不太好。
还有最重要的一点是,获取鼠标位置,并且设置相应的弓箭朝向。响应点击事件来射箭,具体也就是调用Controller的接口来执行动作。
游戏效果
本次实验到此结束!
最后
以上就是热情玉米为你收集整理的【Unity3d学习】使用物理引擎——打飞碟游戏的物理引擎改进与射箭游戏设计写在前面HitUFO的物理引擎改进版本射箭游戏设计与实现的全部内容,希望文章能够帮你解决【Unity3d学习】使用物理引擎——打飞碟游戏的物理引擎改进与射箭游戏设计写在前面HitUFO的物理引擎改进版本射箭游戏设计与实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复