VR游戏开发302 开发一款VR弹球游戏(中)

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

设置你的弹球

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

弹球参数

新建一个Material,名为M_Ball。

新建材质

将Shader选为Mobile Bump Specular

选择Mobile的Shader

将材质球拖到Sphere上,并调整材质到满意。此步也可以省略。至于场景优化等问题将在

后面的教程里面详细介绍。

必要的游戏管理者

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

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

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

脚本初步内容如下:

publicclassGameStatus:MonoBehaviour{

stringgameStatus;

publicstringgetGameStatus()

{

returngameStatus;

}

publicvoidsetGameStatus(stringgs)

{

gameStatus= gs;

}

// Use this for initialization

voidStart () {

gameStatus ="begin";

}

}

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

让你的球动起来

为了让我们的弹球能够动起来便需要给弹球加上一个脚本,这个脚本负责控制球是否可以移动和球的移动方向。

我们新建一个叫做ballMove.cs的C#脚本并赋予Ball物体。

给Ball附上脚本

双击打开脚本编辑器。

写入以下代码

publicclassballMove:MonoBehaviour{

Vector3direction;

GameObjectgm;

GameStatusgs;

publicfloatspeed = 0.1f;

//用于设置球移动的方向

publicvoidsetDirection(Vector3dir)

{

direction= dir;

}

//外部用于获取球移动的方向

publicVector3getDirection()

{

returnthis.direction;

}

//为了节约性能不适用Update函数而使用FixedUpdate函数

voidFixedUpdate()

{

if(gs.getGameStatus()!="win"&& gs.getGameStatus() !="lose")

{

if(gs.getGameStatus() =="begin")

{

transform.Translate(direction * speed);

}

elseif(gs.getGameStatus()=="going")

{

transform.Translate(direction * speed);

}

}

//Debug.Log(direction);

}

// Use this for initialization

voidStart () {

gm =GameObject.FindGameObjectWithTag("GameManager");

gs =gm.GetComponent();

direction=newVector3(0.5f,0,-1);

}

}

注意,这些代码不是一成不变的,我们以后需要增加新功能的时候会对本代码进行修改。

以上代码的功能仅仅是根据GameManager中的GameStatus来决定球体移动与否,由于我写的代码不是很漂亮,所以读者们可自己发挥。

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

脚本内容如下:

publicclassGameStatus:MonoBehaviour{

stringgameStatus;

publicstringgetGameStatus()

{

returngameStatus;

}

publicvoidsetGameStatus(stringgs)

{

gameStatus = gs;

}

// Use this for initialization

voidStart () {

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必须勾选)

在ColliderB上面添加RigidBody

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

勾选IsKinematic

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

添加完毕之后,在ColliderB物体上新建一个P_ColliderB的脚本。

该脚本内容如下:

publicclassP_ColliderB:MonoBehaviour{

GameObjectgm;

ballMovebm;

publicstringcolliderType;

publicVector3playerLeftDir;

// Use this for initialization

voidStart () {

gm =GameObject.FindGameObjectWithTag("Ball");

bm =gm.GetComponent();

}

voidOnTriggerEnter(ColliderOther)

{

if(Other.tag=="Ball")

{

if(colliderType =="ColliderB")

{

bm.setDirection(newVector3(bm.getDirection().x, bm.getDirection().y,-bm.getDirection().z));

//Debug.Log("Entered");

}

elseif(colliderType=="Edge")

{

bm.setDirection(newVector3(-bm.getDirection().x, bm.getDirection().y,bm.getDirection().z));

}

elseif(colliderType=="player.colliderA")

{

bm.setDirection(newVector3(playerLeftDir.x,bm.getDirection().y,playerLeftDir.z));

}

elseif(colliderType =="player.colliderC")

{

bm.setDirection(newVector3(-playerLeftDir.x, bm.getDirection().y,playerLeftDir.z));

}

}

}

}

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

一开始的设置

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

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

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

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

对于向量进行分析

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

我们新建一个脚本,名为directionVector.cs。

namespaceangle2Vector

{

publicclassdirectionVector:MonoBehaviour

{

publicVector3returnVector(floatangle,floatlength)

{

Vector3dir=newVector3();

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;

returndir;

}

}

}

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

给左右两侧的Edge添加碰撞

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

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

最后,我们新建一个Cube放在对面,作为测试用的平台,用来代替对手。

Cube的设置如下图所示:

Cube的设置

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

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

完成啦

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

ballMove.cs

usingUnityEngine;

usingSystem.Collections;

usingangle2Vector;

publicclassballMove:MonoBehaviour{

Vector3direction;

GameObjectgm;

GameStatusgs;

publicfloatspeed = 0.1f;

//用于设置球移动的方向

publicvoidsetDirection(Vector3dir)

{

direction = dir;

}

//外部用于获取球移动的方向

publicVector3getDirection()

{

returnthis.direction;

}

//为了节约性能不适用Update函数而使用FixedUpdate函数

voidFixedUpdate()

{

if(gs.getGameStatus()!="win"&& gs.getGameStatus() !="lose")

{

if(gs.getGameStatus() =="begin")

{

transform.Translate(direction * speed);

}

elseif(gs.getGameStatus()=="going")

{

transform.Translate(direction * speed);

}

}

//Debug.Log(direction);

}

// Use this for initialization

voidStart () {

gm =GameObject.FindGameObjectWithTag("GameManager");

gs =gm.GetComponent();

directionVectordv=newdirectionVector();

direction =dv.returnVector(250,1);

}

}

playerMovement.cs

usingUnityEngine;

usingSystem.Collections;

publicclassplayerMovement:MonoBehaviour{

GameObjectmainCamera;

RayCasterrc;

floatthisX;

voidStart()

{

mainCamera =GameObject.FindGameObjectWithTag("MainCamera");

rc =mainCamera.GetComponent();

thisX=this.transform.position.x;

}

voidFixedUpdate()

{

this.transform.position=newVector3(rc.getHisPos()+thisX,this.transform.position.y,this.transform.position.z);

}

}

P_Collider_B.cs

usingUnityEngine;

usingSystem.Collections;

publicclassP_ColliderB:MonoBehaviour{

GameObjectgm;

ballMovebm;

publicstringcolliderType;

publicfloatplayerLeftReflectionAngle;

// Use this for initialization

voidStart () {

gm =GameObject.FindGameObjectWithTag("Ball");

bm =gm.GetComponent();

}

voidOnTriggerEnter(ColliderOther)

{

if(Other.tag=="Ball")

{

if(colliderType =="ColliderB")

{

bm.setDirection(newVector3(bm.getDirection().x, 0, -bm.getDirection().z));

//Debug.Log("Entered");

}

elseif(colliderType=="Edge")

{

bm.setDirection(newVector3(-bm.getDirection().x, 0, bm.getDirection().z));

}

elseif(colliderType=="player.colliderA")

{

angle2Vector.directionVectordv =newangle2Vector.directionVector();

bm.setDirection(dv.returnVector(playerLeftReflectionAngle,1));

}

elseif(colliderType =="player.colliderC")

{

angle2Vector.directionVectordv =newangle2Vector.directionVector();

bm.setDirection(newVector3(-dv.returnVector(playerLeftReflectionAngle, 1).x, 0,dv.returnVector(playerLeftReflectionAngle, 1).z));

}

}

}

}

directionVector.cs

usingUnityEngine;

usingSystem.Collections;

namespaceangle2Vector

{

publicclassdirectionVector:MonoBehaviour

{

publicVector3returnVector(floatangle,floatlength)

{

Vector3dir=newVector3();

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;

returndir;

}

}

}

GameStatus.cs

usingUnityEngine;

usingSystem.Collections;

publicclassGameStatus:MonoBehaviour{

stringgameStatus;

publicstringgetGameStatus()

{

returngameStatus;

}

publicvoidsetGameStatus(stringgs)

{

gameStatus = gs;

}

// Use this for initialization

voidStart () {

gameStatus ="begin";

}

}


本文作者沈庆阳拥有著作权,未经允许不得转载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容