赛车教程pdf3.docx
- 文档编号:4347421
- 上传时间:2022-11-30
- 格式:DOCX
- 页数:12
- 大小:27.40KB
赛车教程pdf3.docx
《赛车教程pdf3.docx》由会员分享,可在线阅读,更多相关《赛车教程pdf3.docx(12页珍藏版)》请在冰豆网上搜索。
赛车教程pdf3
若熟悉了基本的u3d界面,请在此下载赛车教程的资源包和三本原文pdf教程
首先对照下边给出链接中网络上的前两本pdf翻译,完成拼装赛车和参数调整。
这些都进行玩之后,请继续看这本教程。
U3d脚本学习之赛车教程pdf3
赛车教程是u3d实例教程中的第一个,包括三个pdf,如果下载了资源并且按照前两个pdf走了一遍,应该对u3d的模式有了点认识。
强大的用户界面确实很方便,脚本写好后拖拽到对象上就可以用了,而且在用户界面中可以不打开脚本文件,直接修改成员属性。
赛车教程的第三个pdf是关于脚本的,最好能仔细阅读,这里做一点简单翻译,水平有限望大家指正。
另外对于前两个pdf文件,网络上有前辈做了翻译,可以在以下地址找到:
Pdf1:
Pdf2:
引擎盖之下——赛车脚本研究
***************************************************************************
我们已经了解了如何从3d模型,脚本和内建组建创建一个赛车对象,也简单了解了一些可以在用户界面中调整的变量,并且对其如何参与调整赛车的行为有了感性认识。
接下来是做更深的探讨的时间了,让我们了解下赛车内部的构造,看看他的引擎——脚本。
双击car.js,将在脚本编辑器中打开这个文件。
这个程序初看上去有点吓人,500多行的代码,大量的变量和函数。
不必担心,我们的脚本是结构化的,简单易读。
函数的名字简洁并且能够表示它的作用。
加上注释之后,你会发现这些脚本并不凌乱。
对于阅读这些脚本的方法,我们建议您找到一个合适的入口,然后顺序地阅读。
这个文件中的入口是Start()、Update()和FixedUpdate()。
这些“主要函数”均调用了其他的函数,当我们从start()函数开始看的时候,我们会发现它调用了SetupWheelColliders()这个函数。
然后进入这个函数阅读,阅读完这个函数之后返回Start()函数,接下来轮到SetupCenterOfMass()了,我们就这样一个一个函数地阅读,就像程序运行的顺序一样,这样就会对程序运行的流程有一个感性认识了。
我们会这样阅读所有的函数,虽然不必解释每一行程序,但是我们会涉及到程序运行中每一个重要的步骤,从初始化到每一帧中的各种操作。
闲话不叙了,让我们开始吧。
最重要的是什么?
因为我们有方便的编辑器、拖拽的操作方式和内建的组件(components)系统,所以使用unity3d工作很轻松。
U3d管理模型的导入,我们则在几次简单的鼠标点击中完成完成碰撞检测、渲染和物理系统。
在脚本中,我们主要操作这些组件(components),当然你会发现我们为了决定赛车的下一步动作,要做很多的计算,而这些计算在游戏中是完全不可避免的部分。
如果你想做出更复杂的计算,则必须写一些操作逻辑。
实质上简单来说,做这些运算的目的不过是为了给组件一个正确的数值而已。
如果你觉得不知所措,无从下手,那么可以试着以下述问题为核心,并且思考一下为了完成这些问题,我们需要做什么:
刚体
车轮的碰撞检测
我们要做的计算和命令
请思考类似下边的问题:
为我们的赛车模型添加一个刚体组件之后,就可以用物理的方式操作赛车了。
具体的实现方式则是计算赛车的动力和阻力。
通过车轮的碰撞检测,我们可以控制赛车和路面的接触。
Start()——一切的开始:
======================================================================
这个函数是我们对赛车做初始化的地方。
这个函数仅在脚本开始运行的时候执行一次,执行在Update()函数之前。
这个函数一般用来初始化程序的先决条件。
在赛车的脚本中,Start()首先访问的函数是:
SetupWheelColliders()
我们关联到赛车的轮子有四个,这些轮子在检视器(inspector)中被分为“前轮”和“后轮”两个数组。
在这个函数中,我们实现了碰撞器,令车轮和赛车的下表面产生互动。
在这个函数的第一步,我们从访问SetupWheelFrictionCurve()函数开始。
SetupWheelFrictionCurve()
在SetupWheelFrictionCurve()中我们创建了一个新的WheelFrictionCurve,并且为其分配了一个适合我们的赛车的值。
WheelFrictionCurve描述了轮胎的阻力属性,这个属性被车轮碰撞(WheelColliders)所使用。
如果你想利用u3d认真地配置车轮碰撞系统,不妨参考一下文档。
SetupWheel()
在设置好轮胎阻力之后,我们回到了SetupWheelColliders()函数,准备创建碰撞器和轮胎对象。
我们通过对每个车轮调用SetupWheel()函数完成这一步骤。
观察这个函数,你会发现他有两个参数,一个Transform和一个布尔值,它会返回一个轮子(wheel)对象。
我们在执行这个函数时要做的是给出每个轮子的Transform值(在u3d中是描述位置、旋转和缩放属性的对象),并声明这个轮子是前轮还是后轮。
接着函数会返回一个轮子对象,我们把轮子置入用来存储轮子的wheelsarray数组中。
事实上在SetupWeel()函数中我们做的事情很简单,就是创建了一个新的游戏对象(轮子)并且给出它的位置参数。
之后我们为这个对象添加了轮子碰撞检测构件。
我们利用悬挂变量(pdf2中讨论过)调整轮胎碰撞器的属性。
之后我们就使用下列参数创建了一个新的Wheel对象:
我们创建好的碰撞器、轮子阻力(WheelFrictionCurve)、轮子的图形(Graphic对象,即在pdf2中我们配置汽车时拖拽到检视器的DiskBreak对象)以及轮胎的图形(Graphic对象,DistBreak的孩子对象)
我们使用轮胎的大小作参数,自动设置轮子的半径:
wheelRadius=wheel.tireGraphic.renderer.bounds.size.y/2;
最后我们检测一下SetupWheel函数的参数isFrontWheel,如果车轮不是前轮,则将这个车轮的变量“驱动轮(driveWheel)”设定为真值,若是前轮,则将其“转向轮(steerWheel)”设定为真值。
在之后的代码中我们会通过检测轮子与道路的接触判定是否可以驱动赛车或令赛车转向。
赛车仅当至少有一个驱动轮接触地面的时候可以驱动,仅当至少有一个转向轮接触地面的时候可以转向。
之后我们对前轮做一点附加操作,新建了一个游戏对象并将其置于前轮对象和赛车对象之间,这就是将来在赛车转向时转动前轮的转向杆(SteerColumn)。
最后将生成的车轮数组返回给SetupWheelColliders()函数。
当所有车轮配置完毕之后,退出SetupWheelColliders()函数返回到Start()函数。
SetupCenterOfMass()
这是我们要阅读的下一个函数,这个函数很简短,它将刚体的重心设置为我们在pdf2中创建的CenterOfMass对象。
若没有设置重心,则Unity会自动计算默认重心。
之后我们使用一个简单的小函数设定一下赛车的最高速度,将我们在用户界面检视器中手工输入的数字(千米时)转换为米每秒:
topSpeed=Convert_Miles_Per_Hour_To_Meters_Per_Second(topSpeed);
这个函数仅仅是将参数乘上0.44704这个常数,将千米时格式时间转为米每秒。
这样在用户界面检视器中我们可以输入千米时单位时间,并在计算物理系统时使用米每秒单位时间。
我们也有一个相反的转换函数,在游戏要输出赛车速度的时候,相反的转换会被用到。
SetupGears()
赛车的档位是在这个函数中自动设置的,方式是为各个档位设定最高时速,之后计算在每一个不同的档位,要想达到设定的时速需要多大的推力。
推力的计算方式是使用公有变量中设定的空气阻力和惯性作参数,这意味着这个计算只是一个线性的,Z轴方向的计算(无关XY轴,不难理解,赛车换挡和漂移,或者飞上天空没什么直接关系)。
这个力会乘上一个参数,用以确保赛车可以达到一个超过档位最高时速阈值的速度(这里有点不大理解为什么要超过,我猜测是为了换挡吧,这样从低档位可以换到高档位。
求读者指教)
SetupSkidMarks()
这个函数寻找场景中的刹车痕迹(Skidmark)对象,并且保存它们和它们的粒子系统(为烟雾效果准备)的参数,刹车痕迹部分的脚本代码在我们的文件中并没有写,当然有兴趣的话你也可以自己完成它。
在Start()函数的最后,我们将阻力计算器(dragMultiplier,一个描述阻力的数组对象,刚体具备的属性。
这个对象是在pdf2中我们自己做的,代替了原本刚体内建的。
)的x值赋给一个变量:
initialDragMultiplierX=dragMultiplier.x;
我们之所以要储存这个值,因为系统使用dragMultiplier计算赛车阻力,但是在手刹状态需要改变它的值,而回到前进状态的时候需要重新初始化为正常值。
至此,Start()函数中的初始化工作已经全部完毕,之后我们可以下一个重要的函数——Update()了,这个函数是我们脚本的主循环,会在游戏的每一帧执行一次。
Update()——每一帧的动作:
======================================================================
GetInput()
在每一帧的动作(Update()函数)中,我们首先做的事情是获取用户在刚刚这一帧的时间里做了什么操作。
使用GetInput()函数可以做到获取用户操作。
函数的前两行首先取出Input事件的垂直、水平方向操作信息,存入trottle和steer变量:
throttle=Input.GetAxis("Vertical");
steer=Input.GetAxis("Horizontal");
垂直、水平方向的按键配置可以在unity的输入管理器中修改(Edit->Projectsettings->Input),默认情况下垂直方向键位为“w、s”以及“上、下方向键”,而水平方向键位为“a、d”以及“左、右方向键”(和多数的游戏一样)。
这里读取的用户输入信息,稍后将用于控制赛车,垂直方向信息用于做决定赛车引擎推力的油门控制,而水平方向信息则是用于控制赛车转向。
CheckHandBrake()
读取用户输入信息完毕之后我们调用函数CheckHandBrake(),它用来检测玩家是否按下了“空格键”,并且按照不同的检测结果做出相应的逻辑处理。
当用户开始按下空格键的时候,本函数会将变量handbrake置为真值,开启一个计时器记录当前时间,并改变dragMultiplier.x的值(令赛车运动变的不规则,模拟刹车效果)。
之后用户按住空格不放的话,对游戏将不产生影响,因为handbrake值一直为真(参考代码)。
当空格键被释放,若handbrake此时为真值,代码的另一段将被执行,作为协同程序(Coroutine,类似于一个线程,但是具备自己的堆栈和资源,与主程序共享全局变量的程序)的StopHandbraking()将会被执行(执行协同程序的方式,在u3d中如下列代码所示)
StartCoroutine(StopHandbraking(Mathf.Min(5,Time.time-handbrakeTime)));
StopHandbraking()
这个函数有一个参数,是用户按下手刹的时间(若超过5秒则按5秒计),函数利用刹车时间这个参数计算刹车结束后回归正常行驶状态的时间。
这是一个线程函数,函数不停计算当前行驶状态的阻力计算器(dragMultiplier)和正常值(之前存储在initialDragMultiplierX变量中)的差并修改当前dragMultiplier,直到和正常值相等。
作为一个线程方法可以和其他线程并存(所以用了while),当执行到yield语句的时候,会放弃其线程执行权(当速度回归正常之后,后续处理工作不需要很强的实时性,就可以放弃线程执行权了)。
Check_If_Car_Is_Flipped()
回到Update()函数,接下来程序用Check_If_Car_Is_Flipped()函数来执行“Turtle-Check”,这个函数中我们检测了赛车的旋转角度,它可以完美地判断赛车是否翻车或者卡死在地图死角。
比方说我们刚经历了一次撞车或者其他疯狂的事情,那么要检测一下赛车是不是停在了一个让玩家无法控制的位置。
如果赛车无法被继续操作,那么程序会将自上一帧至现在的时间加到变量resetTimer中去。
如果赛车可以被继续操作,我们将resetTimer置为0。
.当resetTimer达到一定的时长(默认值5秒),程序执行函数FlipCar()。
FlipCar()
这个函数的作用很简单,就是令赛车正面朝上,速度置为0,初始化状态。
(类似跑跑卡丁车里R键的作用)
UpdateWheelGraphics()
这是Update()中最长和最复杂的函数了,幸运的是其中有很长一段是关于我们暂时不讲解的刹车痕迹的,除此之外,这段代码中最重要的功能就是更新车轮的位置和旋转角度了。
对于每个轮子,我们从检测它是否接触地面开始。
若判定车轮接触了地面,则将轮子的位置设置为接触点再向上移动一个车轮半径的距离,这样就将车轮放在了合适的地方。
w.wheelGraphic.localPosition=wheel.transform.up*
(wheelRadius+wheel.transform.InverseTransformPoint(wh.point).y);
放置好轮子之后我们获取接触点的刚体的速度(这一块我还有点晕,接触点是一个WheelHit类型的对象,这个对象类型的定义还没有看。
而刚体类型似乎有速度这个属性),并将它转化为车轮本地坐标(函数InverseTransformDirection()的作用是将一个坐标从世界坐标系转化为本地坐标系),作为车轮的速度属性:
w.wheelVelo=rigidbody.GetPointVelocity(wh.point);
w.groundSpeed=w.wheelGraphic.InverseTransformDirection(w.wheelVelo);
如果车轮并没有接触地面,我们要通过两个参数:
车轮在位置变换后的空间位置和悬挂系统参数,来决定车轮的当前位置。
而车轮的运动速度计算方式也有所不同。
接下来是设置车轮的转向位置,如果当前车轮是一个转向轮,那么我们首先要表示它因为转动赛车方向盘产生的左右转向。
通过转动它的父对象——转向杆来控制车轮左右转向。
计算转向角度的方式是使用一个表示车轮转动幅度的,来自玩家输入的参数,乘上预设的最大转动角度。
因为转向轮是转向杆的子对象,所以转向杆转向的时候,转向轮会跟随父对象转动。
之后对于每个轮子(不管是转向轮还是驱动轮),我们要计算因为赛车前行而产生的滚动转向。
计算方式是用前一步中求得的车轮运动速度和车轮的半径,来计算其运动角速度:
w.tireGraphic.Rotate(Vector3.right*(w.groundSpeed.z/wheelRadius)*Time.deltaTime*Mathf.Rad2Deg);
UpdateGear()
在Update()函数中调用的最后一个函数是UpdateGear(),这是一个简单的函数,作用是通过将当前速度和我们在Start()中初始化的档位表作比较,来确定当前所在的档位。
以上就是在Update()函数中,我们脚本所做的所有工作了,它看上去也不那么难不是么?
接下来就要到脚本主循环的其余部分了,这就是在FixedUpdate()函数中,完成物理计算工作的各个函数了。
FixedUpdate()——所有的物理计算都由我来做!
======================================================================
在做物理计算的时候,为了保证结果的平滑流畅,精确地按照帧率的节奏做每帧执行一次的计算可能并不是合适的方式。
而FixedUpdate()函数正是应此而生。
这个函数会被保证每隔一段固定的时间就被调用一次。
关于Update命令的文档会告诉你,FixedUpdate()函数是这样一种函数:
如果帧率比较低,他可以在两帧之间被调用多次;当帧率高的时候,它也可以在游戏运行了好几帧时不被调用。
当这个函数执行时,所有关于物理系统的计算和更新都立即执行。
在FixedUpdate()函数执行时,调用了很多的函数。
这些函数中的每一个都为了计算和执行物理系统中的受力分析。
UpdateDrag()
我们首先访问的函数是UpdateDrag(),drag即是赛车运行时受到的空气阻力。
它对赛车的运行产生反方向的作用。
我们通过赛车在某个轴向速度的平方值来计算赛车受到的阻力。
Vector3(-relativeVelocity.x*Mathf.Abs(relativeVelocity.x),
-relativeVelocity.y*Mathf.Abs(relativeVelocity.y),
-relativeVelocity.z*Mathf.Abs(relativeVelocity.z));
这意味着当我们增加赛车在某轴向的速度的时候,这个方向的阻力也会增加,而使用平方来计算阻力的方式,实际上来自于物理公式。
计算实际阻力的时候我们要借助参数dragMultiplier,这个参数前边已经多次提到。
因为赛车在前方、侧方和上方受到的阻力是完全不同的,所以我们要引入这个参数方便计算。
当手刹状态被打开的时候,我们要对赛车受到的来自前方和侧方的阻力做额外的加成。
这取决于当前赛车的速度和速度调整策略。
请注意我们对赛车前进的方向和和速度做点乘来计算赛车在前方受到的额外阻力,这导致赛车受到的来自前方的阻力被加成,而侧方向的阻力减少。
对于x轴向的阻力也是一样,赛车受到的侧方向阻力越大,我们越是增加其受到的前方的阻力,用减慢赛车速度的方式代替让赛车一直侧滑。
当没有用到手刹的时候,我们只更新x轴向的值:
drag.x*=topSpeed/relativeVelocity.magnitude;
这样的策略也是为了增加赛车的操控性,增加侧向阻力来避免赛车侧滑。
(这一段的翻译有点吃力,估计会有错误,希望大家指正)
函数的最后我们对刚体应用受力:
rigidbody.AddForce(transform.TransformDirection(drag)*rigidbody.mass*Time.deltaTime);
因为阻力和速度方向相反,我们对阻力取相反的变换值,来减慢赛车速度。
UpdateFriction()
这个函数用来计算和应用车轮与地面间的摩擦力。
这个摩擦力不难计算,因为我们可以利用在程序初始化的时候设定的轮胎摩擦力曲线。
这个曲线接受我们输入的滑动值,输出摩擦力值。
摩擦力有两个方向,一个是影响加速和刹车的前方,一个是影响赛车侧滑的侧向。
当我们设定好摩擦力曲线后,它将自动完成轮胎和地面间的摩擦力计算:
w.collider.sidewaysFriction=wfc;
w.collider.forwardFriction=wfc;
在做这些之前,我们要先做一步设置,将侧向摩擦力改为根据赛车侧向速度而增减,这是为了在赛车拐弯时增减侧向摩擦力,防止侧滑。
CalculateEnginePower()
计算引擎马力(稍后被用来计算赛车刚体受到的推进力)也不难理解,但是它有一点怪异:
若没有踩下油门,我们仅在每次执行时减小引擎马力,令赛车渐渐减速。
如果在赛车前进的方向(使用函数HavaSameSigh()判断是否在同一个方向)踩下油门,则要给引擎马力一个额外的加成。
接下来的计算有点乱,我们要计算一个基准马力(计算方式是用当前引擎马力除以最大输出马力,则这个值在0-1之间),之后对这个基准马力乘以二(那么我们得到的马力将在0-2之间)。
之后调用工具函数EvaluateNormPower(),当目前基准马力为0-1之间时这个函数返回一个1-10之间的值,当基准马力在1-2之间时,返回一个0-1之间的值。
你是不是有些晕?
这个返回值用在下面的算式中:
currentEnginePower+=Time.deltaTime*200*EvaluateNormPower(normPower);
最终的计算结果中,当我们的赛车在一个较低速度下,且踩下油门,则需要一个较大的加速度。
但是若赛车已经到达最高速度,这时候踩下油门将不会获得更大的加速了。
这就是为什么在基准马力大的时候EvaluateNormPower()返回值反而小。
若玩家朝向赛车运行相反的方向加速,那么可以认为是刹车。
我们会给引擎马力一个负加成,这个负加成大于玩家什么都不做时引擎马力的自然减小值。
CalculateState()
这是一个简单的小函数,作用很简单,就是判定运行时赛车的车轮是否接触地面。
工作流程如下:
首先将变量canDrive和canSteer设为假值。
之后检测车轮数组中的每个轮子,是否接触地面:
w.collider.isGrounded
如果车轮接触地面,检测车轮类型,若是驱动轮则设定canDrive为真值,若是转向轮则设置canSteer为真。
这个函数的执行效果是,当至少有一个驱动轮接触地面的时候,赛车可以驱动,当至少有一个转向轮接触地面的时候,赛车可以转向。
接下来就是整个脚本的最后两个函数了,这两个函数是真正将我们之前做的物理计算应用到赛车对象的刚体中去的。
因此我们在读这一段代码的时候也会更加细致。
这两个函数令赛车可以真正地被驱动和转向。
ApplyThrottle()
仅当CalculateState()函数中设定了canDrive变量为真,即至少有一个驱动轮着地的状况下本函数才执行算法。
若当前赛车是canDrive状态,我们首先将从用户输入中读取的input信息中用户油门变量和当前赛车速度中相对赛车前方的速度分量(relativeVelocity.z)作比较。
若油门和当前速度具有相同的符号(通过工具函数HaveSameSign()做判断),说明当前油门加速方向和赛车前进方向一致,那么我们就要将油门给予的马力加成到赛车刚体受力中去,首先计算出油门马力值:
throttleForce=Mathf.Sign(throttle)*currentEnginePower*rigidbody.mass;
若油门没有活动(玩家按下了手刹,屏蔽了油门),油门变量值为-1,会给赛车一个负方向的油门马力加成。
相反若玩家按下了加速,油门马力会正向加成到刚体中去,令赛车加速。
若油门方向是和赛车前进方向相反(玩家按下了向后的按键),也就相当于刹车。
这时候那么就要对赛车刚体中加成一个负方向的消
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 赛车 教程 pdf3
![提示](https://static.bdocx.com/images/bang_tan.gif)