不少童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此咱们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给你们,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 移动 - 推球:游戏中的物理php
控制刚体球体的速度。css
经过跳跃支持垂直运动。nginx
检测地面及其角度。apache
使用ProBuilder建立测试场景。编程
沿斜坡移动。json
这是有关控制角色移动的教程系列的第二部分。此次,咱们将使用物理引擎建立更逼真的运动并支持更复杂的环境。bash
本教程使用Unity 2019.2.11f1制做。它还使用ProBuilder软件包。微信
最终效果之一框架
在不公平的赛道上不受约束的球体。less
刚体
在上一教程中,咱们将球体约束为保留在矩形区域内。显式地编程这样的限制颇有意义,由于它很简单。可是,若是咱们但愿球体在复杂的3D环境中移动,则必须支持与任意几何图形的交互。咱们将使用Unity现有的物理引擎,即NVIDIA的PhysX,而不是本身实现。
与物理引擎结合使用,有两种通用的方法来控制角色。首先是刚体方法,即经过施加力或改变其速度,使角色的行为像常规物理对象同样,而间接控制它。第二种是运动学方法,即在仅查询物理引擎执行自定义碰撞检测的同时进行直接控制。
刚体组件
咱们将使用第一种方法来控制球体,这意味着咱们必须向其中添加一个Rigidbody
组件。咱们可使用刚体的默认配置。
添加该份量足以将咱们的球体变成一个物理对象,只要它仍然具备其SphereCollider
份量便可。从如今开始,咱们推迟到物理引擎进行碰撞,所以从中删除区号Update
。
Vector3 newPosition = transform.localPosition + displacement;//if (newPosition.x < allowedArea.xMin) {// newPosition.x = allowedArea.xMin;// velocity.x = -velocity.x * bounciness;//}//…transform.localPosition = newPosition;
消除了咱们本身的约束后,球体再次能够自由移动通过平面的边缘,在此点,球体因为重力而直线降低。发生这种状况是由于咱们从不覆盖球体的Y位置。
咱们再也不须要容许区域的配置选项。咱们的自定义跳动也再也不须要。
//[SerializeField, Range(0f, 1f)]//float bounciness = 0.5f;//[SerializeField]//Rect allowedArea = new Rect(-5f, -5f, 10f, 10f);
若是咱们仍然想约束球体保留在平面上,则能够经过添加其余对象来阻止其路径来实现。例如,建立四个立方体,对其进行缩放和定位,以便它们围绕平面造成一堵墙。这将防止球体掉落,尽管它在与墙壁碰撞时表现得很怪异。因为此时咱们具备3D几何形状,所以再次启用阴影以更好地了解深度也是一个好主意。
物理怪异。
当试图移动到一个角落时,因为物理引擎和咱们本身的代码争夺球形的位置,所以球形变得不稳定。咱们将其移入墙壁,而后PhysX经过将其向后推来解决碰撞。若是咱们中止将其推入墙壁,则PhysX将使球因为动量而保持运动。
控制刚体速度
若是要使用物理引擎,则应让它控制球体的位置。直接调整位置将有效地传送,这不是咱们想要的。相反,咱们必须经过对球施加力或调整其速度来间接控制球。
咱们已经对位置进行了间接控制,由于咱们会影响速度。咱们要作的就是更改代码,使其覆盖Rigidbody
组件的速度,而不是本身调整位置。咱们须要为此访问组件,所以经过body
在Awake
方法中初始化的字段来跟踪它。
Rigidbody body;void Awake () {body = GetComponent<Rigidbody>();}
从Update中删除位移代码,而是将咱们的速度分配给body的速度。
//Vector3 displacement = velocity * Time.deltaTime;//Vector3 newPosition = transform.localPosition + displacement;//transform.localPosition = newPosition;body.velocity = velocity;
可是物理碰撞等也会影响速度,所以请先将其从body中检索出来,而后再对其进行调整以匹配所需的速度。
velocity = body.velocity;float maxSpeedChange = maxAcceleration * Time.deltaTime;velocity.x =Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);velocity.z =Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);body.velocity = velocity;
控制body的速度。
无摩擦运动
如今,咱们调整球体的速度,PhysX用来移动它。而后解决冲突,能够调整速度,而后再次调整速度,依此类推。尽管球体更加缓慢而且没有达到其最大速度,但最终的运动看起来像咱们之前的运动。那是由于PhysX会产生摩擦。尽管这更现实,但它使配置球体变得更加困难,所以让咱们消除摩擦和反弹。这是经过“ 资产/建立/物理材质”建立新的物理材质(是的,在菜单中拼写为“ Physic”),而后将全部值设置为零,将“ 合并”模式设置为“ 最小”。
将此物理材质分配给球体的对撞机。
如今,它再也不受到任何摩擦或反弹。
不建议不要直接调节速度吗?
这是基于速度瞬时变化是不现实的想法的通用建议。咱们正在作的是有效地施加加速度,只是以一种受控的方式来达到目标速度。若是您知道本身在作什么,直接调整速度就能够了。
无摩擦运动。
与球体碰撞时,球体彷佛仍会反弹一点。发生这种状况是由于PhysX不会阻止碰撞,而是会在碰撞发生后检测到它们,而后移动刚体以使它们再也不相交。在快速运动的状况下,这可能须要一个以上的物理模拟步骤,所以咱们能够看到这种穿透现象的发生。
若是运动确实很是快,那么球体可能最终会彻底穿过壁或朝另外一侧穿透,这对于较薄的壁来讲更可能发生。您能够经过更改的Rigidbody碰撞检测模式来避免这种状况,但这一般仅在移动很是快时才须要。
并且,球体如今能够滑动而不是滚动,所以咱们也能够冻结其在全部尺寸上的旋转,这能够经过组件的“ 约束”复选框来完成Rigidbody
。
固定更新
物理引擎使用固定的时间步长,而无论帧速率如何。尽管咱们已经将球的控制权交给了PhysX,但咱们仍然会影响其速度。为了得到最佳结果,咱们应该以固定的时间步长同步调整速度。为此,咱们将Update
方法分为两部分。咱们检查输入并设置所需速度的部分能够保留在Update中,而速度的调整应移至新FixedUpdate
方法。为了完成这项工做,咱们必须将所需的速度存储在一个场中。
Vector3 velocity, desiredVelocity;
void Update () {Vector2 playerInput;playerInput.x = Input.GetAxis("Horizontal");playerInput.y = Input.GetAxis("Vertical");playerInput = Vector2.ClampMagnitude(playerInput, 1f);//Vector3 desiredVelocity =desiredVelocity =new Vector3(playerInput.x, 0f, playerInput.y) * maxSpeed;}void FixedUpdate () {velocity = body.velocity;float maxSpeedChange = maxAcceleration * Time.deltaTime;velocity.x =Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);velocity.z =Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);body.velocity = velocity;}
FixedUpdate在每一个物理模拟步骤的开始都调用该方法。发生的频率取决于时间步长,默认为0.02(每秒50次),可是您能够经过“ 时间”项目设置或经过更改时间步长Time.fixedDeltaTime
。
根据您的帧速率FixedUpdate
,每次调用能够调用0次,一次或屡次Update
。每一个框架都会发生一系列FixedUpdate
调用,而后Update
被调用,而后呈现框架。当物理时间步长相对于帧时间太大时,这可使物理仿真的离散性质变得明显。
0.2物理时间步。
您能够经过减小固定时间步长或启用的Rigidbody插值模式来解决此问题。将其设置为Interpolate可以使它在其最后位置和当前位置之间线性插值,所以根据PhysX,它会稍微落后于其实际位置。另外一个选项是Extrapolate,它根据其速度插值到其猜想的位置,这仅对于速度基本恒定的对象才真正可接受。
带插值的0.2物理时间步长。
请注意,增长时间步长意味着球体在每次物理更新时覆盖的距离更大,这可能致使使用离散碰撞检测时球体穿过壁隧穿。
跳跃
因为咱们的球体如今能够在3D物理世界中导航,所以咱们可使其具备跳跃的能力。
根据指令跳跃
咱们能够用Input.GetButtonDown("Jump")来检测玩家是否按下了该帧的跳转按钮,默认状况下是空格键。咱们在Update中这样作,可是就像调整速度同样,咱们会将实际的跳跃延迟到FixedUpdate的下次调用。所以,请经过布尔字段desiredJump跟踪是否须要跳转。
bool desiredJump;…void Update () {…desiredJump = Input.GetButtonDown("Jump");}
可是,咱们可能最终不调用FixedUpdate下一帧,在这种状况下desiredJump将其调回false原定位置,而desiredJump 将被遗忘。咱们能够经过布尔“或”运算或“或”分配将检查与其先前的值相结合来防止这种状况。这样,它将保持true启用状态,直到咱们将其显式设置回false。
desiredJump|=Input.GetButtonDown("Jump");
在调整速度以后和在FixedUpdate中应用速度以前,请检查是否须要跳跃。若是是这样,请重置desiredJump
并调用一个新Jump
方法,该方法最初仅将5添加到速度的Y份量,以模拟忽然的向上加速度。
void FixedUpdate () {…if (desiredJump) {desiredJump = false;Jump();}body.velocity = velocity;}void Jump() {velocity.y += 5f;}
这将使球体向上移动,直到因为重力不可避免地回落。
跳。
跳跃高度
让咱们对其范围进行配置是可配置的。咱们能够经过直接控制跳跃速度来作到这一点,但这并不直观,由于初始跳跃速度和跳跃高度之间的关系并不微不足道。直接控制跳跃高度更方便,因此让咱们开始吧。
[SerializeField, Range(0f, 10f)]float jumpHeight = 2f;
跳跃须要克服重力,所以所需的垂直速度取决于重力。特别,vÿ=--2GHv_y = sqrt(-2gh) 那里 GG 是重力, HH是所需的高度。负号在那里,由于GG假定为负。咱们能够经过检索它Physics.gravity.y
,也能够经过Physics项目设置进行配置。咱们正在使用默认的重力矢量,该矢量向下垂直为9.81,与地球的平均重力匹配。
void Jump () {velocity.y +=Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);}
如何得出所需的速度?
咱们从初始跳跃速度开始 Ĵ,它会因重力而减少,直到达到零,而后咱们开始回落。重力G是一个持续不断的加速度,将咱们拉倒,为此咱们在此推导中使用正数,由于这使咱们免于编写大量负号。因此在任什么时候候Ť 由于跳跃的垂直速度是 v = jg t。何时v达到零,咱们位于跳跃的顶部,所以正好位于所需的高度。这发生在jg t = 0,因此何时 j = gt。所以,当t = j /克。
由于 G 恒定,任什么时候候的平均速度为 v_(av)= j-(gt)/ 2,所以随时的高度为 h = v_(av)t = jt-(gt ^ 2)/ 2。这意味着在跳跃的顶端h = j(j / g)-(g(j / g)^ 2)/ 2,咱们能够重写为 h = j ^ 2 / g-(j ^ 2 / g)/ 2 = j ^ 2 / g-j ^ 2 /(2g)= j ^ 2 /(2g)
如今咱们知道 h = j ^ 2 /(2g) 在顶部,所以 j ^ 2 = 2gh 和 j = sqrt(2gh)。何时G 是负数而不是 j = sqrt(-2gh)。
请注意,因为物理模拟的离散性,咱们极可能没法达到所需的高度。在时间步长之间的某个地方将达到最大值。
在地面的跳跃
目前,咱们能够随时跳下,即便已经在空中,也能够永远保持空中飞行。仅当球体在地面上时才能启动适当的跳跃。咱们没法直接询问Rigidbody
它当前是否正在接触地面,可是当它与某些物体碰撞时咱们会获得通知,所以咱们将使用它。
若是MovingSphere
有一个OnCollisionEnter
方法,那么它将在PhysX检测到新的碰撞后被调用。只要物体保持彼此接触,碰撞就仍然存在。以后,OnCollisionExit
将调用一个方法(若是存在)。将两种方法都添加到MovingSphere中,将第一个 onGround
boolean字段设置为true
,并将后者 boolean字段设置为false
。
bool onGround;…void OnCollisionEnter () {onGround = true;}void OnCollisionExit () {onGround = false;}
如今咱们只能在地面上跳跃,如今咱们假设在触摸某物时就是这种状况。若是咱们不接触任何东西,则应忽略指望的跳跃。
void Jump () {if (onGround) {velocity.y += Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);}}
当球体仅接触地面时,此方法有效,但若是它也短暂接触墙,则跳跃将变得不可能。之因此发生这种状况,是由于OnCollisionExit
在咱们仍与地面保持接触的同时,它被做为墙壁使用。解决方案是不依赖OnCollisionExit
而是添加一种OnCollisionStay
方法,只要碰撞仍然存在,就能够在每一个物理步骤中调用该方法。设置onGround
于true
在该方法中。
//void OnCollisionExit () {// onGround = false;//}void OnCollisionStay () {onGround = true;}
每一个物理步骤都从调用全部FixedUpdate
方法开始,而后PhysX完成其工做,最后调用碰撞方法。所以,若是存在任何活动冲突,则在最后一步FixedUpdate期间将设置什么时候调用gets 。为了保持onGround有效,咱们要作的就是在FixedUpdate末尾将其onGround设置为false。
void FixedUpdate () {…onGround = false;}
如今,只要咱们接触到某物,咱们就能够跳跃。
无墙跳跃
当触摸任何东西时都容许跳跃意味着咱们也能够在空中但触摸墙壁而不是地面时跳跃。若是要防止这种状况,咱们必须可以区分地面和其余东西。
将地面定义为主要是水平面是有意义的。咱们能够经过检查碰撞接触点的法线向量来检查咱们所碰撞的物体是否知足此条件。
什么是法向量?
它是指示方向的单位长度向量。一般是远离某物的方向。所以,一个平面只有一个法向量,而球体上的每一个点都有一个指向其中心的不一样法线向量。
一个简单的碰撞只有两个形状接触的单个点,例如,当咱们的球体接触地面时。一般,球体会稍微穿透平面,而PhysX经过将球体直接推离平面而解决了。推进的方向是接触点的法线向量。由于咱们使用的是球体,因此矢量始终从球体表面上的接触点指向其中心。
实际上,它可能比这更混乱,由于可能存在多个碰撞,而且穿透可能会持续一个以上的仿真步骤,可是咱们如今没必要真正担忧这一点。咱们确实须要认识到的是,一次碰撞能够包含多个接触。对于平面-球体碰撞,这是不可能的,可是当涉及到凹形网格对撞机时,这是可能的。
咱们能够经过向和Collision
都添加一个参数来获取碰撞信息。与其直接设置onGround 为true,咱们不如将责任转交给一种新方法EvaluateCollision ,并将数据给它。
void OnCollisionEnter (Collision collision) {//onGround = true;EvaluateCollision(collision);}void OnCollisionStay (Collision collision) {//onGround = true;EvaluateCollision(collision);}void EvaluateCollision (Collision collision) {}
能够经过Collision的contactCount
属性找到接触点的数量。咱们可使用它经过该GetContact
方法遍历全部点,并为其传递索引。而后,咱们能够访问该点的normal
属性。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;}}
法线是球应被推进的方向,该方向直接远离碰撞表面。假设它是一个平面,则矢量与平面的法向矢量匹配。若是平面是水平的,则其法线将指向垂直,所以其Y份量应正好为1。若是是这种状况,则咱们正在接触地面。可是,咱们要宽容一些,接受0.9或更大的Y份量。
Vector3 normal = collision.GetContact(i).normal;onGround |= normal.y >= 0.9f;
空中跳跃
在这一点上,咱们只能在地面上跳,可是游戏一般容许空中跳两次甚至三跳。让咱们对此进行支持,并使其可配置为容许多少次空气跳跃。
[SerializeField, Range(0, 5)]int maxAirJumps = 0;
如今,咱们必须跟踪跳转阶段,以便知道是否容许再次跳转。若是咱们在地面上,咱们能够经过在FixedUpdate开始时将其设置为零的整数字段来执行此操做。可是,让咱们将代码与速度检索一块儿移动到单独的UpdateState
方法中,以保持FixedUpdate
简短。
int jumpPhase;…void FixedUpdate () {//velocity = body.velocity;UpdateState();…}void UpdateState () {velocity = body.velocity;if (onGround) {jumpPhase = 0;}}
从如今开始,每次跳跃时,咱们都会增长跳跃阶段。咱们能够在地面上或还没有达到容许的最大空中跳跃时跳跃。
void Jump () {if (onGround|| jumpPhase < maxAirJumps) {jumpPhase += 1;velocity.y += Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);}}
应该<= maxAirJumps不是吗?
跳转后,跳转阶段当即设置回零。在下一个教程中,咱们将找到缘由。
限制向上速度
快速连续跳跃的空气使向上的速度比单次跳跃的速度高得多。咱们将进行更改,以使咱们不能超过单跳便可达到所需高度的跳速。第一步是隔离计算出的跳跃速度Jump
。
jumpPhase += 1;float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);velocity.y +=jumpSpeed;
若是咱们已经有向上的速度,则在将其添加到速度的Y份量以前,将其从跳跃速度中减去。这样,咱们将永远不会超过跳跃速度。
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);if (velocity.y > 0f) {jumpSpeed = jumpSpeed - velocity.y;}velocity.y += jumpSpeed;
可是,若是咱们已经快于跳跃速度,那么咱们不但愿跳跃使咱们减速。咱们能够经过确保修改后的跳转速度永远不会变为负值来避免这种状况。经过采用修改后的最大跳跃速度和零来实现。
if (velocity.y > 0f) {jumpSpeed =Mathf.Max(jumpSpeed - velocity.y, 0f);}
空中运动
目前,咱们在控制球体时不在意球体是在地面上仍是在空中,但能够理解,空中球体更难控制。控制的数量能够在彻底控制和彻底控制之间变化。这取决于游戏。所以,经过添加单独的最大空气加速度(默认设置为1),使它可配置。这样能够大大减小空中控制,但不能彻底将其删除。
[SerializeField, Range(0f, 100f)]float maxAcceleration = 10f, maxAirAcceleration = 1f;
如今,咱们在FixedUpdate计算最大速度变化时使用哪一种加速度取决于咱们是否在地面上。
float acceleration = onGround ? maxAcceleration : maxAirAcceleration;float maxSpeedChange =acceleration* Time.deltaTime;
连续下坡
咱们正在使用物理学在一个小的平面上移动球体,与墙碰撞并四处跳跃。一切都很好,所以是时候考虑更复杂的环境了。在本教程的其他部分中,咱们将研究涉及坡度时的基本运动。
ProBuilder测试场景
您能够经过旋转平面或立方体来建立坡度,但这是建立关卡的不便方法。所以,咱们将导入ProBuilder程序包,并使用该程序包建立一些坡度。该ProGrids包也驾轻就熟栅格捕捉,但若是你碰巧使用,它不是在统一2019.3须要。ProBuilder使用起来至关简单,可是可能须要一些时间来适应。我不会解释如何使用它,只是要记住,它主要是关于脸的,而边缘和顶点是次要的。
我从ProBuilder立方体开始建立了一个坡度,将其拉伸到10×5×3,在X维度上将其拉伸了10个单位,而后将X面折叠到其底部边缘。这将产生一个三角形的双斜面,其两侧的斜率长为10个单位,高为5个单位。
我将其中十个放置在一个平面上,并将它们的高度从一单位更改成十个单位。包括平坦的地面在内,咱们得到的倾斜角度大约为0.0°,5.7°,11.3°,16.7°,21.8°,26.6°,31.0°,35.0°,38.7°,42.0°和45.0°。
以后,我又放置了十个斜坡,此次是从45°版本开始,而后将笔尖向每一个倾斜的角度向左拉一个单位,直到最后获得一面垂直墙。这给咱们提供了大约48.0°,51.3°,55.0°,59.0°,63.4°,68.2°,73.3°,78.7°,84.3°和90.0°的角度。
经过将球体变成预制件并添加21个实例(从每一个水平到彻底垂直),每一个坡度一个实例,我完成了测试场景。
若是您不想本身设计关卡,能够从本教程的资源库中获取它。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-02-physics/
斜率测试
由于全部球体实例都响应用户输入,因此咱们能够同时控制它们。这样就能够当即测试与多个倾斜角度相互做用时球体的行为。对于大多数这些测试,我将进入播放模式,而后连续按向右键。
斜率测试。
使用默认球体配置,咱们能够看到前五个球体以几乎彻底相同的水平速度移动,而与倾斜角无关。第六个几乎没有通过,而其他的则回滚或被陡峭的斜坡彻底挡住了。
由于大多数球体都有效地结束了飞行,因此咱们将最大空气加速度设置为零。这样,咱们只有在考虑到基础上才考虑加速。
空气加速与零空气加速之间的差别并不重要,由于它们飞出了斜坡。可是第六球如今再也不到达另外一侧,其余球也因为重力而提早中止。发生这种状况是由于它们的坡度太陡而没法保持足够的动力。在第六球的状况下,其空气加速度足以将其推向上方。
接地角
目前,咱们使用0.9做为阈值来将某物归类为不归类,但这是任意的。咱们可使用0–1范围内的任何阈值。尝试两个极端会产生很是不一样的结果。
让咱们经过控制最大地面角度使阈值可配置,由于最大地面角度比坡度法线向量的Y份量更直观。让咱们使用25°做为默认值。
[SerializeField, Range(0f, 90f)]float maxGroundAngle = 25f;
当表面水平时,其法线向量的Y份量为1。对于彻底垂直的墙,Y份量为零。Y份量根据倾斜角度在这些极端之间变化:它是该角度的余弦。咱们在这里处理单位圆,其中Y是垂直轴,水平轴位于XZ平面中的某个位置。另外一种说法是,咱们正在查看向上矢量和表面法线的点积。
组态的角度定义了仍算做地面的最小结果。让咱们的门槛存储在一个领域,并经过Mathf.Cos计算它的一个OnValidate
方法。这样,当咱们在播放模式下经过检查器更改角度时,它将保持与角度同步。同时Awake调用它,以便在构建中对其进行计算。
float minGroundDotProduct;void OnValidate () {minGroundDotProduct = Mathf.Cos(maxGroundAngle);}void Awake () {body = GetComponent<Rigidbody>();OnValidate();}
咱们以度为单位指定角度,但Mathf.Cos但愿将其表示为弧度。咱们能够经过乘以Mathf.Deg2Rad将其转换。
minGroundDotProduct = Mathf.Cos(maxGroundAngle* Mathf.Deg2Rad);
如今咱们能够调整最大地面角度,看看它如何影响球体的运动。从如今开始,我将角度设置为40°。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;onGround |= normal.y >=minGroundDotProduct;}}
在斜坡上跳跃
不管当前球面的角度如何,咱们的球体始终会直线向上跳跃。
另外一种方法是沿法线向量的方向跳离地面。每一个坡度测试车道都会产生不一样的跳跃,因此让咱们这样作。
咱们须要跟踪一个领域中的当前接触法线,并在遇到地面接触EvaluateCollision时将其存储起来。
Vector3 contactNormal;…void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;//onGround |= normal.y >= minGroundDotProduct;if (normal.y >= minGroundDotProduct) {onGround = true;contactNormal = normal;}}}
可是,咱们最终可能没有触及地面。在这种状况下,咱们将使用up向量做为接触法线,所以空气跳跃仍然会直线上升。若是须要,将其在UpdateState中设置。
void UpdateState () {velocity = body.velocity;if (onGround) {jumpPhase = 0;}else {contactNormal = Vector3.up;}}
如今,咱们必须将按跳跃速度缩放的跳跃接触法线添加到跳跃时的速度上,而不是始终仅增长Y份量。这意味着跳跃高度表示咱们在平坦地面或仅在空中时跳跃的距离。在斜坡上跳跃不会达到很高,但会影响水平速度。
void Jump () {if (onGround || jumpPhase < maxAirJumps) {…//velocity.y += jumpSpeed;velocity += contactNormal * jumpSpeed;}}
但这意味着对垂直速度为正的检查也再也不正确。它必须成为检查与接触法线对齐速度的方法。咱们能够经过将速度投影到接触法线上并经过计算它们的点积Vector3.Dot来找到该速度。
float jumpSpeed = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);float alignedSpeed = Vector3.Dot(velocity, contactNormal);if (alignedSpeed> 0f) {jumpSpeed = Mathf.Max(jumpSpeed -alignedSpeed, 0f);}velocity += contactNormal * jumpSpeed;
如今,这些跳跃与坡度对齐,咱们的测试场景中的每一个球体都具备惟一的跳跃轨迹。陡峭的斜坡上的球再也不直接跳入其斜坡,而是随着跳跃将球朝与运动相反的方向推进而变慢。您能够经过大幅下降最大速度来尝试在全部斜坡上更清楚地看到这一点。
沿着斜坡移动
到目前为止,不管倾斜角度如何,咱们始终在水平XZ平面中定义所需的速度。若是球体沿坡度上升,那是由于PhysX将球向上推以解决发生的碰撞,由于咱们给它指定了指向坡度的水平速度。在上坡时,这能够很好地工做,可是在下坡时,球体会远离地面移动,而且当它们的加速度足够高时最终会掉落。结果是难以控制的弹性运动。在上坡时反转方向时,尤为是在将最大加速度设置为较高值时,您能够清楚地看到这一点。
失去接地;最大加速度100。
咱们能够经过将所需速度与地面对齐来避免这种状况。它的工做方式与咱们在法线上投影速度以得到跳跃速度的方式相似,只是如今咱们必须在平面上投影速度才能获取新速度。咱们经过像之前同样取向量和法线的点积,而后从原始速度向量中减去由该法线缩放的法线来作到这一点。让咱们为使用任意矢量参数的方法建立一个方法ProjectOnContactPlane。
Vector3 ProjectOnContactPlane (Vector3 vector) {return vector - contactNormal * Vector3.Dot(vector, contactNormal);}
为何不使用Vector3.ProjectOnPlane?
该方法执行相同的操做,但不假定提供的法向向量具备单位长度。它将结果除以法线的平方长度(一般为1,所以不须要)。
让咱们建立一个新方法AdjustVelocity来调整速度。首先经过在接触平面上投影右向向量和向前向量来肯定投影的X轴和Z轴。
void AdjustVelocity () {Vector3 xAxis = ProjectOnContactPlane(Vector3.right);Vector3 zAxis = ProjectOnContactPlane(Vector3.forward);}
这使咱们的向量与地面对齐,可是当地面彻底平坦时,它们只有单位长度。一般,咱们必须对向量进行归一化以得到正确的方向。
Vector3 xAxis = ProjectOnContactPlane(Vector3.right).normalized;Vector3 zAxis = ProjectOnContactPlane(Vector3.forward).normalized;
如今,咱们能够将当前速度投影到两个向量上,以得到相对的X和Z速度。
Vector3 xAxis = ProjectOnContactPlane(Vector3.right).normalized;Vector3 zAxis = ProjectOnContactPlane(Vector3.forward).normalized;float currentX = Vector3.Dot(velocity, xAxis);float currentZ = Vector3.Dot(velocity, zAxis);
咱们能够像之前同样使用它们来计算新的X和Z速度,可是如今相对于地面。
float currentX = Vector3.Dot(velocity, xAxis);float currentZ = Vector3.Dot(velocity, zAxis);float acceleration = onGround ? maxAcceleration : maxAirAcceleration;float maxSpeedChange = acceleration * Time.deltaTime;float newX =Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);float newZ =Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);
最后,经过沿相对轴添加新旧速度之间的差别来调整速度。
float newX =Mathf.MoveTowards(currentX, desiredVelocity.x, maxSpeedChange);float newZ =Mathf.MoveTowards(currentZ, desiredVelocity.z, maxSpeedChange);velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
FixedUpdate
代替旧的速度调节代码,调用此新方法。
void FixedUpdate () {UpdateState();AdjustVelocity();//float acceleration = onGround ? maxAcceleration : maxAirAcceleration;//float maxSpeedChange = acceleration * Time.deltaTime;//velocity.x =// Mathf.MoveTowards(velocity.x, desiredVelocity.x, maxSpeedChange);//velocity.z =// Mathf.MoveTowards(velocity.z, desiredVelocity.z, maxSpeedChange);if (desiredJump) {desiredJump = false;Jump();}body.velocity = velocity;onGround = false;}
与地面保持一致;最大加速度100。
使用咱们新的速度调整方法,当在斜坡上忽然忽然反转方向时,球再也不与地面失去接触。除此以外,因为指望速度会调整其方向以匹配斜率,所以如今每一个车道都会改变绝对指望水平速度。
请注意,若是坡度未与X轴或Z轴对齐,则相对投影轴之间的角度将不为90°。除非斜坡很是陡峭,不然这并非很明显。您仍然能够在全部方向上移动,可是要精确地在某些方向上进行导航比在其余方向上更难。这在某种程度上模仿了试图穿越但不与陡坡对齐的尴尬。
多个地面法线
当只有一个地面接触点时,使用接触法线来调整所需的速度和跳跃方向效果很好,可是当同时存在多个地面接触时,行为可能会变得奇怪且不可预测。为了说明这一点,我建立了另外一个测试场景,该测试场景的地面有些凹陷,一次最多能够有四个接触点。
跳跃时,球体会朝哪一个方向前进?就我而言,拥有四个联系人的人倾向于偏向一个方向,但最终会朝四个不一样方向前进。一样,具备两个接触的球体在两个方向之间任意拾取。具备三个接触的球始终以相同的方式跳跃,以匹配仅接触单个坡度的附近球。
出现这种现象的缘由是,只要咱们发现地面接触点,便将法线设置为EvaluateCollision。所以,若是咱们发现多个,则最后一个赢。因为移动的顺序是任意的,或者因为PhysX计算碰撞的顺序,顺序老是相同的。
哪一个方向最好?没有一个。将它们所有组合成一个表明平均接地平面的法线是最有意义的。为此,咱们必须累积法线向量。这就要求咱们在FixedUpdate的末尾将接触法线设置为零。让咱们将代码与onGround重置一块儿放入新方法ClearState中。
void FixedUpdate () {…body.velocity = velocity;//onGround = false;ClearState();}void ClearState () {onGround = false;contactNormal = Vector3.zero;}
如今在EvaluateCollision累积法线而不是覆盖前一个法线。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;if (normal.y >= minGroundDotProduct) {onGround = true;contactNormal+=normal;}}}
最后,将UpdateState中在地面上的接触法线归一化以使其成为适当的法线向量。
void UpdateState () {velocity = body.velocity;if (onGround) {jumpPhase = 0;contactNormal.Normalize();}else {contactNormal = Vector3.up;}}
地面接触点计算
虽然不是必需的,但咱们能够算出咱们有多少个地面接触点,而不只仅是跟踪是否至少有一个。咱们经过将布尔字段替换为整数来作到这一点。而后,咱们引入一个布尔型只读属性OnGround(注意大小写),该属性检查计数是否大于零,并替换该onGround
字段。
//bool onGround;int groundContactCount;bool OnGround => groundContactCount > 0;
该代码如何工做?
这是定义单语句只读属性的一种简便方法。与如下内容相同:
bool
OnGround {
get
{
return
groundContactCount > 0;
}
}
ClearState
如今必须将计数设置为零。
void ClearState () {//onGround = false;groundContactCount = 0;contactNormal = Vector3.zero;}
而且UpdateState
必须依靠属性而不是字段。除此以外,咱们还能够经过仅对接触法线进行归一化(若是是聚合的话)进行归一化来进行一些优化,不然它已是单位长度了。
void UpdateState () {velocity = body.velocity;if (OnGround) {jumpPhase = 0;if (groundContactCount > 1) {contactNormal.Normalize();}}…}
还要在Evaluate
适当的时候增长计数。
void EvaluateCollision (Collision collision) {for (int i = 0; i < collision.contactCount; i++) {Vector3 normal = collision.GetContact(i).normal;if (normal.y >= minGroundDotProduct) {//onGround = true;groundContactCount += 1;contactNormal += normal;}}}
最后,用OnGround
的AdjustVelocity
和Jump
更换onGround
。
除了UpdateState中地面接触数量的优化,对调试也颇有用。例如,您能够记录计数或根据计数调整球体的颜色,以更好地了解其状态。
您是如何改变颜色的?
我将如下代码添加到Update:
GetComponent<Renderer>().material.SetColor(
"_Color", Color.white * (groundContactCount * 0.25f)
);
假定球体的材质具备_Color属性,默认渲染管线的标准着色器就是这种状况。若是您使用的是Lightweight / Universal管道的默认着色器,则须要使用_BaseColor。
下一个教程是表面接触(Surface Contact)。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-02-physics/
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来确定用得着
声明:发布此文是出于传递更多知识以供交流学习之目的。如有来源标注错误或侵犯了您的合法权益,请做者持权属证实与咱们联系,咱们将及时更正、删除,谢谢。
原做者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/physics/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes