文章目录
射箭游戏设计与实现
游戏要求:
游戏内容要求:
靶对象为 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的接口来执行动作。
游戏效果
本次实验到此结束!