使用Cardboard SDK开发一款VR弹球游戏 —— 弹球功能实现

『泰然网』原创,转载请注明出处。
作者:@沈庆阳

上一期教程中,我们设置好了一个可以跟随玩家视线移动的平台。下面便到了制作我们的弹球的过程了。由于本篇文章有部分脚本在文章的进行过程中会发展,请参考文章末尾给出的所有脚本。

设置你的弹球

在Unity中新建一个Sphere球体,将Position设为(0,0,0),同时将Scale设为(0.6,0.6,0.6)。

新建一个Material,名为M_Ball。

将Shader选为Mobile Bump Specular

将材质球拖到Sphere上,并调整材质到满意。此步也可以省略。至于场景优化等问题将在
后面的教程里面详细介绍。

必要的游戏管理者

游戏管理者是游戏制作中必须的一部分,当然在一些微型游戏中,游戏管理者(GameManager也许并非那么必要)。

在你的游戏中写入GameManager是个好的习惯,让GameManager来管理一些游戏数据,像是游戏状态(GameStatus),分数(Score),S&L(Save and Load)等,这样可以节约很大的时间成本,让你的游戏编写更加有条理。

那么我们开始编写一个简单的GameManager。首先新建一个Object名为GameManager。在其上增加一个脚本GameStatus.cs

脚本初步内容如下:

public class GameStatus : MonoBehaviour {
    string gameStatus;
    public string getGameStatus()
    {
        return gameStatus;
    }
    public void setGameStatus(string gs)
    {
        gameStatus = gs;
    }
    // Use this for initialization
    void Start () {
        gameStatus = "begin";
    }
}

GameStatus这个类主要用于管理你现有的游戏状态,当然,游戏状态可以用enum来定义,也可以使用自定的字符串,在这里使用的是自定的字符串。

让你的球动起来

为了让我们的弹球能够动起来便需要给弹球加上一个脚本,这个脚本负责控制球是否可以移动和球的移动方向。
我们新建一个叫做ballMove.cs的C#脚本并赋予Ball物体。

双击打开脚本编辑器。
写入以下代码

public class ballMove : MonoBehaviour {
    Vector3 direction;
    GameObject gm;
    GameStatus gs;
    public float speed = 0.1f;
    //用于设置球移动的方向
    public void setDirection(Vector3 dir)
    {
        direction = dir;
    }
    //外部用于获取球移动的方向
    public Vector3 getDirection()
    {
        return this.direction;
    }
    //为了节约性能 不适用Update函数而使用FixedUpdate函数
    void FixedUpdate()
    {
        if(gs.getGameStatus()!="win" && gs.getGameStatus() != "lose")
        {
            if(gs.getGameStatus() == "begin")
            {
                transform.Translate(direction * speed);
            }
            else if(gs.getGameStatus()=="going")
            {
                transform.Translate(direction * speed);
            }
        }
        //Debug.Log(direction);
    }
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("GameManager");
        gs = gm.GetComponent<GameStatus>();
        direction=new Vector3(0.5f,0,-1);
    }
}

注意,这些代码不是一成不变的,我们以后需要增加新功能的时候会对本代码进行修改。
以上代码的功能仅仅是根据GameManager中的GameStatus来决定球体移动与否,由于我写的代码不是很漂亮,所以读者们可自己发挥。

至此我们的BallMove脚本还存在错误,这是因为GameStatus还没有设置。现在,我们新建一个GameObject名为GameManager,在GameManager上新建一个GameStatus的脚本,负责管理游戏状态。

脚本内容如下:

public class GameStatus : MonoBehaviour {
    string gameStatus;
    public string getGameStatus()
    {
        return gameStatus;
    }
    public void setGameStatus(string gs)
    {
        gameStatus = gs;
    }
    // Use this for initialization
    void Start () {
        gameStatus = "begin";
    }
}

GameStatus脚本使用字符串记录游戏状态,其他类可以获取和修改游戏状态。游戏状态在游戏开始时为begin状态。

完成以上的步骤之后,点击Play按钮,你的球是不是动起来了呢?对!但是球直接飞过了你移动的平台,这是因为我们使用的是Translate而不是物理引擎,所以Collider碰撞体对我们的球没有效果。

让你的球可以反弹

既然选择不用Unity自带的物理引擎,我们就得想办法获得球的运行方向和反射方向和何时来更改球的运动轨迹。弹球只有在碰撞到平台或是边缘的时候才会反弹,所以我们需要设置相应的碰撞体为Trigger。

首先我们要搞懂Unity的坐标系。以我们新建的平台以及球为视角(如下图)。根据小球的坐标轴以找到X和Z轴的正方向(因为Y轴是上下移动,在这里我们不需要球的上下移动所以忽略Y坐标)。

画出球的坐标系来算出反射角和入射角的X及Z关系。

如上图,在游戏中我们有两种碰撞情况。一种是平台的碰撞,反射平面为X。另一种是墙壁碰撞,反射面是Z。在平台碰撞的时候入射角和反射角的计算以X为基础来计算,在该基础下,反射向量的X坐标不变,Z坐标变为-Z。同理,以Z为反射平面来算的话,Z坐标不变,X坐标变为-X。

在游戏中,我们使用一个向量来决定弹球的运动方向。那么因此,反射的模型也建设完毕了。下面我们开始设置碰撞体的Trigger。

选中我们的Player的Prefab,展开阶层面板,选中ColliderB(中间的Collider,注意该Collider的isTrigger必须勾选)

在其上添加一个RigidBody(刚体),并勾选IsKinematic

此处如果不添加刚体的话,对于移动物体来说,OnTriggerEnter的判断不会成功,所以此处必须添加一个刚体。然而我们只是添加一个刚体,并没有使用物理引擎。

添加完毕之后,在ColliderB物体上新建一个P_ColliderB的脚本。
该脚本内容如下:

public class P_ColliderB : MonoBehaviour {
    GameObject gm;
    ballMove bm;
    public string colliderType;
    public Vector3 playerLeftDir;
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("Ball");
        bm = gm.GetComponent<ballMove>();
    }
    void OnTriggerEnter(Collider Other)
    {
        if(Other.tag=="Ball")
        {
            if (colliderType == "ColliderB")
            {
                bm.setDirection(new Vector3(bm.getDirection().x, bm.getDirection().y, -bm.getDirection().z));
                //Debug.Log("Entered");
            }
            else if(colliderType=="Edge")
            {
                bm.setDirection(new Vector3(-bm.getDirection().x, bm.getDirection().y, bm.getDirection().z));
            }
            else if(colliderType=="player.colliderA")
            {
                bm.setDirection(new Vector3(playerLeftDir.x,bm.getDirection().y,playerLeftDir.z));
            }
            else if (colliderType == "player.colliderC")
            {
                bm.setDirection(new Vector3(-playerLeftDir.x, bm.getDirection().y, playerLeftDir.z));
            }
        }
    }
}

为了便于该脚本的复用,我们设置了一个colliderType来判断该碰撞体是设置在什么地方的。该脚本预留了一个公共变量,用来设置左侧的碰撞体碰撞的后球的方向。这里我的设置是如下图。

不过数据也可以经过一次次调试来确定。有个问题,因为向量的大小,即|A| A为向量。决定着球的运行速度,所以这里向量的模应和发球速度的向量的模相等,否则会造成每次碰撞平台的边缘,造成球速的增加或减少。

所以这时我们回到ballMove的脚本中,将初始的向量重新设计。我们将球移动的向量的长度即模设为1,由此再计算X和Z的相应大小。

但由于我们都是程序员,肯定不能依靠每次都用笔去算,所以这时,修改以上的程序,再写一个用于生成向量的类似乎是最佳之选。

所以,我们再以X轴正方向为起点,规定角度的形式,如下图:

而我们现在的工作是,给定模长和角度,输出X和Z的坐标。如果未给定模长,则以1为默认模长。这里会用到高中数学的正弦余弦的知识,具体不再解释,会直接放出程序代码的。
我们新建一个脚本,名为directionVector.cs

namespace angle2Vector
{
    public class directionVector : MonoBehaviour
    {
        public Vector3 returnVector(float angle,float length)
        {
            Vector3 dir=new Vector3();
            dir.x = (float)System.Math.Cos(angle* System.Math.PI / 180) * length;
            dir.z = (float)System.Math.Sin(angle* System.Math.PI / 180) * length;
            dir.y = 0;
            return dir;
        }
    }
}


最后,我们将左右两侧的Edge本身的碰撞体改为Trigger并添加RigidBody后勾选IsKenematic。

将P_Collider B脚本拖到EdgeL和EdgeR的上面,在ColliderType处填写Edge。

同时对于Player的ColliderA和ColliderC的设置如下图所示

最后,我们新建一个Cube放在对面,作为测试用的平台,用来代替对手。
Cube的设置如下图所示:

最后,点击Play按钮,按住Alt移动你的视线,这样你就可以和一面墙玩弹球游戏啦!

当然,我们不可能对着一面墙玩弹球游戏,这样你永远不可能赢。在下一节中,我们将完成这个游戏,写一个简单的AI对手和游戏的状态机。

最后,放上所有本节中的完整代码

ballMove.cs

using UnityEngine;
using System.Collections;
using angle2Vector;
public class ballMove : MonoBehaviour {
    Vector3 direction;
    GameObject gm;
    GameStatus gs;
    public float speed = 0.1f;
    //用于设置球移动的方向
    public void setDirection(Vector3 dir)
    {
        direction = dir;
    }
    //外部用于获取球移动的方向
    public Vector3 getDirection()
    {
        return this.direction;
    }
    //为了节约性能 不适用Update函数而使用FixedUpdate函数
    void FixedUpdate()
    {
        if(gs.getGameStatus()!="win" && gs.getGameStatus() != "lose")
        {
            if(gs.getGameStatus() == "begin")
            {
                transform.Translate(direction * speed);
            }
            else if(gs.getGameStatus()=="going")
            {
                transform.Translate(direction * speed);
            }
        }
        //Debug.Log(direction);
    }
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("GameManager");
        gs = gm.GetComponent<GameStatus>();
        directionVector dv=new directionVector();
        direction =dv.returnVector(250,1);
    }
}

playerMovement.cs

using UnityEngine;
using System.Collections;
public class playerMovement : MonoBehaviour {
    GameObject mainCamera;
    RayCaster rc;
    float thisX;
    void Start()
    {
        mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
        rc = mainCamera.GetComponent<RayCaster>();
        thisX = this.transform.position.x;
    }
    void FixedUpdate()
    {
        this.transform.position=new Vector3(rc.getHisPos()+thisX,this.transform.position.y,this.transform.position.z);
    }
}

P_Collider_B.cs

using UnityEngine;
using System.Collections;
public class P_ColliderB : MonoBehaviour {
    GameObject gm;
    ballMove bm;
    public string colliderType;
    public float playerLeftReflectionAngle;
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("Ball");
        bm = gm.GetComponent<ballMove>();
    }
    void OnTriggerEnter(Collider Other)
    {
        if(Other.tag=="Ball")
        {
            if (colliderType == "ColliderB")
            {
                bm.setDirection(new Vector3(bm.getDirection().x, 0, -bm.getDirection().z));
                //Debug.Log("Entered");
            }
            else if(colliderType=="Edge")
            {
                bm.setDirection(new Vector3(-bm.getDirection().x, 0, bm.getDirection().z));
            }
            else if(colliderType=="player.colliderA")
            {
                angle2Vector.directionVector dv = new angle2Vector.directionVector();
                bm.setDirection(dv.returnVector(playerLeftReflectionAngle,1));
            }
            else if (colliderType == "player.colliderC")
            {
                angle2Vector.directionVector dv = new angle2Vector.directionVector();
                bm.setDirection(new Vector3(-dv.returnVector(playerLeftReflectionAngle, 1).x, 0, dv.returnVector(playerLeftReflectionAngle, 1).z));
            }
        }
    }
}

directionVector.cs

using UnityEngine;
using System.Collections;
namespace angle2Vector
{
    public class directionVector : MonoBehaviour
    {
        public Vector3 returnVector(float angle,float length)
        {
            Vector3 dir=new Vector3();
            dir.x = (float)System.Math.Cos(angle* System.Math.PI / 180) * length;
            dir.z = (float)System.Math.Sin(angle* System.Math.PI / 180) * length;
            dir.y = 0;
            return dir;
        }
    }
}

GameStatus.cs

using UnityEngine;
using System.Collections;
public class GameStatus : MonoBehaviour {
    string gameStatus;
    public string getGameStatus()
    {
        return gameStatus;
    }
    public void setGameStatus(string gs)
    {
        gameStatus = gs;
    }
    // Use this for initialization
    void Start () {
        gameStatus = "begin";
    }
}

标签: unity, vr

?>