交互入门2——射击打靶游戏

文章目录

射箭游戏设计与实现

游戏要求:

游戏内容要求:

  1. 靶对象为 5 环,按环计分;

  2. 箭对象,射中后要插在靶上

    增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后

  3. 游戏仅一轮,无限 trials;
    增强要求:添加一个风向和强度标志,提高难度

具体实现思路:

  1. 设计扁平圆柱体作为靶标,多个不同大小的圆柱体叠加形成不同的环,并利用颜色区分,由于叠加会影响正常显示,所以每个圆柱体的宽度(高)需要不一致,或者利用位置不同实现一个层级的效果,小的在前,大的在后就能显示出一个个环的效果。
  2. 由于对游戏轮次没有太大的要求,所以这里设置为游戏开始时拥有一定数量的箭,箭用完就算结束,显示得分,由用户决定是否再来一次(对于无限trails的规则不太清楚)。
  3. 风向则可是使用一个持续添加在箭上的力来实现。

具体实现代码

首先用到之前几个游戏的一些基类: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的接口来执行动作。

游戏效果

交互入门2——射击打靶游戏

本次实验到此结束!

上一篇:jQuery轮播图的事项 代码详细,容易理解。。。。谢谢观赏


下一篇:假期水题~