????????很多童鞋沒有系統(tǒng)的Unity3D游戲開發(fā)基礎(chǔ),也不知道從何開始學(xué)的榛。為此我們精選了一套國外優(yōu)秀的Unity3D游戲開發(fā)教程琼了,翻譯整理后放送給大家,教您從零開始一步一步掌握Unity3D游戲開發(fā)夫晌。?本文不是廣告雕薪,不是推廣,是免費的純干貨晓淀!本文全名:喵的Unity游戲開發(fā)之路 - 移動?-?游泳 - 在水中移動和漂浮
檢測水量所袁。
施加水阻力和浮力。
在水上游泳凶掰,包括上下游泳燥爷。
使物體漂浮。
這是關(guān)于控制角色移動的系列教程的第九部分懦窘。它可以漂浮在水中并在水中移動前翎。
本教程使用Unity 2019.4.1f創(chuàng)建。它還使用ProBuilder軟件包畅涂。
Unity升級
我已升級到Unity 2019.4 LTS和ProBuilder 4.2.3版本港华,因此某些視覺效果已更改。
效果之一
水
很多游戲都含有水午衰,而且通常都可以游泳立宜。但是冒萄,沒有針對互動式水的開箱即用的解決方案。PhysX不直接支持它橙数,因此我們必須自己創(chuàng)建一個近似的水尊流。
水景
為了演示水,我創(chuàng)建了一個包含游泳池的場景灯帮。它具有各種岸邊配置奠旺,兩個水平面,兩個水隧道施流,一個水橋以及可以在水底行走的地方响疚。我們的水也可以在任意重力下工作,但是此場景使用簡單的均勻重力瞪醋。
水面由具有半透明藍色材料的單面扁平網(wǎng)制成忿晕。從上方可見,但從下方看不到银受。
必須使用設(shè)置為觸發(fā)器的對撞機來描述水的體積践盼。我在大多數(shù)體積中都使用了不帶網(wǎng)孔的箱式對撞機,縮放比例略大于所需的體積宾巍,因此水中不會有任何縫隙咕幻。一些地方需要更復(fù)雜的ProBuilder網(wǎng)格以適合體積。還必須將其設(shè)置為觸發(fā)器顶霞,這可以通過ProBuilder窗口中的“ 設(shè)置觸發(fā)器”選項來完成肄程。請注意,作為觸發(fā)器的網(wǎng)格碰撞器必須是凸形的选浑。凹面網(wǎng)格會自動生成將其包裹起來的凸面版本蓝厌,但會導(dǎo)致它戳出所需水量的地方。彎曲的水橋就是一個例子古徒,為此我制作了一個簡化的凸對撞機拓提。
忽略觸發(fā)器碰撞器
所有水體積對象都在“ 水”層上,應(yīng)將其排除在運動球體和軌道攝影機的所有層蒙版中隧膘。即使到那時代态,通常我們目前擁有的兩個物理查詢也僅用于常規(guī)對撞機,而不是觸發(fā)器疹吃”囊桑可以通過“ 物理/查詢命中觸發(fā)器”項目設(shè)置來配置是否檢測到觸發(fā)器。但是我們永遠都不想使用代碼來檢測觸發(fā)器互墓,因為我們現(xiàn)在擁有什么必尼,因此無論項目設(shè)置如何蒋搜,我們都將其明確化篡撵。
第一個查詢在MovingSphere.SnapToGround中判莉。將
QueryTriggerInteraction.Ignore
作為最終參數(shù)添加到ray cast。
if (!Physics.Raycast(
body.position, -upAxis, out RaycastHit hit,
probeDistance, probeMask, QueryTriggerInteraction.Ignore
)) {
return false;
}
其次育谬,對OrbitCamera.LateUpdate中BoxCast執(zhí)行相同操作券盅。
if (Physics.BoxCast(
castFrom, CameraHalfExtends, castDirection, out RaycastHit hit,
lookRotation, castDistance, obstructionMask,
QueryTriggerInteraction.Ignore
)) {
rectPosition = castFrom + castDirection * hit.distance;
lookPosition = rectPosition - rectOffset;
}
檢測水
現(xiàn)在,我們可以移動水膛檀,好像它不存在一樣锰镀。但是要支持游泳,我們必須檢測到它咖刃。我們將通過檢查是否在“ 水”層上的觸發(fā)區(qū)域內(nèi)來完成此操作泳炉。首先,在MovingSphere中添加水面罩以及游泳材料嚎杨,我們將用它來證明它在水中花鹅。
[SerializeField]
LayerMask probeMask = -1, stairsMask = -1, climbMask = -1, waterMask = 0;
[SerializeField]
Material
normalMaterial = default,
climbingMaterial = default,
swimmingMaterial = default;
然后添加一個InWater
指示球體是否在水中的屬性。首先枫浙,我們將其設(shè)為一個簡單的get / set屬性刨肃,并在?ClearState中
將其重置為false
。
bool InWater { get; set; }
…
void ClearState () {
…
InWater = false;
}
如果我們不攀爬箩帚,請在Update中使用該屬性選擇中的游泳材料真友。
void Update () {
…
meshRenderer.material =
Climbing ? climbingMaterial :
InWater ? swimmingMaterial :normalMaterial;
}
最后,通過添加OnTriggerEnter
和OnTriggerStay
方法完成對水的檢測紧帕。它們的工作方式OnCollisionEnter
與OnCollisionStay
相同盔然,不同之處在于它們適用于對撞機,并且具有Collider
參數(shù)而不是Collision
是嗜。兩種方法都應(yīng)檢查對撞機是否在水層上轻纪,如果設(shè)置IsSwimming
為true
。
void OnTriggerEnter (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
InWater = true;
}
}
void OnTriggerStay (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
InWater = true;
}
}
何時調(diào)用觸發(fā)方法叠纷?
所有觸發(fā)方法都在所有碰撞方法之前被調(diào)用刻帚。
淹沒
僅僅知道我們的球體是否與水相交,還不足以使其正常游泳或漂浮涩嚣。我們需要知道其中有多少被淹沒崇众,然后我們可以用它來計算阻力和浮力。
浸沒程度
讓我們添加一個淹沒浮點字段來跟蹤球體的淹沒狀態(tài)航厚。值零表示沒有水接觸顷歌,而值1表示完全在水下。然后進行更改InWater
幔睬,使其僅返回淹沒是否為正眯漩。在ClearState中將其設(shè)置回零。
bool InWater=> submergence > 0f;
float submergence;
…
void ClearState () {
…
//InWater = false;
submergence = 0f;
}
更改觸發(fā)器方法,以便它們調(diào)用新EvaluateSubmergence
方法赦抖,該方法現(xiàn)在僅將淹沒設(shè)置為1舱卡。
void OnTriggerEnter (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
EvaluateSubmergence();
}
}
void OnTriggerStay (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
EvaluateSubmergence();
}
}
void EvaluateSubmergence () {
submergence = 1f;
}
淹沒范圍
我們將使淹沒范圍可配置。這樣队萤,我們可以精確地控制何時球體算在水中以及何時完全浸入水中轮锥。我們從球體中心上方的一個偏移點開始測量,一直到最大范圍要尔。這樣一來舍杜,即使我們接觸水面,也可以在整個球體進入該區(qū)域之前將其完全淹沒赵辕,或者完全忽略水坑之類的低水位既绩。
使偏移量和范圍可配置。使用0.5和1作為默認值还惠,以匹配我們的半徑0.5球體的形狀熬词。范圍應(yīng)為正。
[SerializeField]
float submergenceOffset = 0.5f;
[SerializeField, Min(0.1f)]
float submergenceRange = 1f;
現(xiàn)在吸重,我們必須在EvaluateSubmergence中
使用水罩執(zhí)行從偏移點一直向下直至浸入范圍的射線投射互拾。在這種情況下,我們確實想擊中水嚎幸,請使用QueryTriggerInteraction.Collide
颜矿。然后,浸入等于1減去擊中距離除以范圍嫉晶。
void EvaluateSubmergence () {
if (Physics.Raycast(
body.position + upAxis * submergenceOffset,
-upAxis, out RaycastHit hit, submergenceRange,
waterMask, QueryTriggerInteraction.Collide,
)) {
submergence = 1f- hit.distance / submergenceRange;
}
}
要測試浸水值骑疆,請使用它為球臨時著色。
void Update () {
…
meshRenderer.material =
Climbing ? climbingMaterial :
InWater ? swimmingMaterial : normalMaterial;
meshRenderer.material.color = Color.white * submergence;
}
這一直到球體完全浸沒的那一刻起作用替废,因為從那時起箍铭,我們從已經(jīng)在水對撞器內(nèi)部的點開始投射,因此射線投射無法擊中它椎镣。但這意味著我們已經(jīng)完全浸入水中诈火,因此我們只要不打任何東西就可以將浸入設(shè)為1。
void EvaluateSubmergence () {
if (Physics.Raycast(
body.position + upAxis * submergenceOffset,
-upAxis, out RaycastHit hit, submergenceRange,
waterMask, QueryTriggerInteraction.Collide
)) {
submergence = 1f - hit.distance / submergenceRange;
}
else {
submergence = 1f;
}
}
但是状答,由于身體位置與PhysX檢測到觸發(fā)時的位置不同冷守,因此從水中移出時可能會導(dǎo)致無效的1淹沒,這是由于碰撞和觸發(fā)方法的調(diào)用延遲所致惊科。我們可以通過將射線的長度增加一個單位來防止這種情況拍摇。這不是完美的,但幾乎可以解決所有情況馆截,除非移動速度非吵浠睿快。退出水時,這將導(dǎo)致浸水變?yōu)樨撝祷炻眩@很好映穗,因為這不算在水中。
void EvaluateSubmergence () {
if (Physics.Raycast(
body.position + upAxis * submergenceOffset,
-upAxis, out RaycastHit hit, submergenceRange+ 1f,
waterMask, QueryTriggerInteraction.Collide
)) {
submergence = 1f - hit.distance / submergenceRange;
}
else {
submergence = 1f;
}
}
現(xiàn)在我們可以擺脫淹沒可視化了淮菠。
//meshRenderer.material.color = Color.white * submergence;
請注意男公,此方法假定球的中心正下方有水荤堪。當(dāng)球體碰到水體積的側(cè)面或底部時(例如合陵,碰到不真實的水墻時),情況可能并非如此澄阳。在這種情況下拥知,我們立即進入完全淹沒狀態(tài)。
水拖
與水相比碎赢,水的運動更為緩慢低剔,因為水比空氣造成更大的阻力。因此肮塞,加速明顯較慢襟齿,而減速較快。讓我們添加對此的支持枕赵,并通過添加水拖動選項(默認設(shè)置為1)使其可配置猜欺。零到10的范圍是可以的,因為10會引起巨大的阻力拷窜。
[SerializeField, Range(0f, 10f)]
float waterDrag = 1f;
我們將使用簡單的線性阻尼开皿,類似于PhysX。我們將速度縮放1減去阻力乘以時間增量篮昧。在FixedUpdate中調(diào)用AdjustVelocity之前進行此操作。我們首先應(yīng)用阻力,所以總是可以加速伴嗡。
void FixedUpdate () {
Vector3 gravity = CustomGravity.GetGravity(body.position, out upAxis);
UpdateState();
if (InWater) {
velocity *= 1f - waterDrag * Time.deltaTime;
}
AdjustVelocity();
…
}
請注意迅皇,這意味著如果水阻力等于1除以固定時間步長,則速度會在單個物理步長中下降為零酵颁。如果速度變大狈孔,速度將反轉(zhuǎn)。由于我們將最大值設(shè)置為10材义,因此這不會成為問題均抽。為了安全起見,可以確保速度至少縮放為零其掂。
如果我們沒有完全淹沒油挥,那么我們就不會遇到最大的阻力。因此,因素會浸入阻尼中深寥。
velocity *= 1f - waterDrag *submergence *Time.deltaTime;
浮力
水的另一個重要屬性是事物傾向于將其漂浮在水中攘乒。因此,應(yīng)向我們的球體添加一個可配置的浮力值惋鹅,該浮力值的最小值為零则酝,默認值為1。該想法是闰集,浮力值為零的物體像石頭一樣下沉沽讹,只是被水拖慢了速度。浮力為1的對象處于平衡狀態(tài)武鲁,完全消除了重力爽雄。浮力大于1的物體會浮到水面。2的浮力意味著它的上升和正常下降一樣快沐鼠。
[SerializeField, Min(0f)]
float buoyancy = 1f;
我們通過在FixedUpdate中檢查是否不是在攀登但在水中來實現(xiàn)這一點挚瘟。如果是這樣,請應(yīng)用按1減去浮力標定的重力饲梭,然后再次考慮浸入乘盖。這將覆蓋重力的所有其他應(yīng)用。
if (Climbing) {
velocity -=
contactNormal * (maxClimbAcceleration * 0.9f * Time.deltaTime);
}
else if (InWater) {
velocity +=
gravity * ((1f - buoyancy * submergence) * Time.deltaTime);
}
else if (OnGround && velocity.sqrMagnitude < 0.01f) { … }
請注意憔涉,實際上向上的力會隨著深度的增加而增加订框,而在我們的情況下,一旦達到最大浸入力监氢,向上的力就保持恒定布蔗。這足以產(chǎn)生令人信服的浮力,除非在極深的水中玩耍浪腐。
浮力似乎失敗的唯一情況是球體最終距離底部太近纵揍。在這種情況下,地面彈跳被激活议街,抵消了浮力泽谨。如果我們在水中,我們可以通過中止SnapToGround來避免這種情況特漩。
bool SnapToGround () {
if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2|| InWater) {
return false;
}
…
}
游泳
現(xiàn)在我們可以在水中漂浮了吧雹,下一步就是支持游泳,其中應(yīng)該包括潛水和浮潛涂身。
游泳門檻
我們只有在水深的情況下才能游泳,但是我們不需要完全浸入水中蛤售。因此丁鹉,讓我們添加一個可配置的游泳閾值妒潭,該閾值定義游泳所需的最小浸入度。它必須大于零揣钦,因此使用0.01–1作為其范圍雳灾,默認值為0.5。如果球體的至少下半部在水下冯凹,則可以使球體游泳谎亩。還添加一個Swimming
指示是否達到游泳閾值的屬性。
[SerializeField, Range(0.01f, 1f)]
float swimThreshold = 0.5f;
…
bool Swimming => submergence >= swimThreshold;
在Update進行調(diào)整宇姚,以便僅在游泳時使用游泳材料匈庭。
void Update () {
…
meshRenderer.material =
Climbing ? climbingMaterial :
Swimming? swimmingMaterial : normalMaterial;
}
接下來,創(chuàng)建一個CheckSwimming
方法空凸,該方法返回我們是否正在游泳嚎花,如果是寸痢,則將地面接觸計數(shù)設(shè)置為零呀洲,并使接觸法線等于上軸。
bool CheckSwimming () {
if (Swimming) {
groundContactCount = 0;
contactNormal = upAxis;
return true;
}
return false;
}
在UpdateState中
檢查我們是否接地時啼止,在CheckClimbing之后直接調(diào)用該方法道逗。這樣一來,除了攀登外献烦,游泳凌駕一切滓窍。
if (
CheckClimbing() ||CheckSwimming() ||
OnGround || SnapToGround() || CheckSteepContacts()
) { … }
然后從SnapToGround中取出檢查放在水中。這樣一來巩那,當(dāng)我們在水中而不是在游泳時吏夯,捕捉動作就會再次起作用。
//if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2 || InWater) {
if (stepsSinceLastGrounded > 1 || stepsSinceLastJump <= 2) {
return false;
}
游泳速度
添加可配置的游泳最大速度和加速度即横,默認情況下均設(shè)置為5噪生。
[SerializeField, Range(0f, 100f)]
float maxSpeed = 10f, maxClimbSpeed = 4f, maxSwimSpeed = 5f;
[SerializeField, Range(0f, 100f)]
float
maxAcceleration = 10f,
maxAirAcceleration = 1f,
maxClimbAcceleration = 40f,
maxSwimAcceleration = 5f;
在AdjustVelocity中,檢查爬升后是否在水中东囚。如果是這樣跺嗽,請使用與通常情況相同的軸使用游泳加速度和速度。
if (Climbing) {
acceleration = maxClimbAcceleration;
speed = maxClimbSpeed;
xAxis = Vector3.Cross(contactNormal, upAxis);
zAxis = upAxis;
}
else if (InWater) {
acceleration = maxSwimAcceleration;
speed = maxSwimSpeed;
xAxis = rightAxis;
zAxis = forwardAxis;
}
else {
acceleration = OnGround ? maxAcceleration : maxAirAcceleration;
speed = OnGround && desiresClimbing ? maxClimbSpeed : maxSpeed;
xAxis = rightAxis;
zAxis = forwardAxis;
}
我們在水中越深页藻,我們應(yīng)該更多地依賴游泳的加速度和速度而不是常規(guī)的速度和速度桨嫁。因此,我們將基于游泳因子在常規(guī)值和游泳值之間進行插值份帐,該因子是淹沒除以游泳閾值璃吧,且最大值限制為1。
else if (InWater) {
float swimFactor = Mathf.Min(1f, submergence / swimThreshold);
acceleration =Mathf.LerpUnclamped(
maxAcceleration,maxSwimAcceleration, swimFactor
);
speed =Mathf.LerpUnclamped(maxSpeed,maxSwimSpeed, swimFactor);
xAxis = rightAxis;
zAxis = forwardAxis;
}
其他加速度是正常加速度還是空氣加速度取決于我們是否在地面上废境。
acceleration = Mathf.LerpUnclamped(
OnGround ?maxAcceleration: maxAirAcceleration,
maxSwimAcceleration, swimFactor
);
潛水和堆焊
現(xiàn)在畜挨,我們可以像在地面或空中一樣在游泳時移動爷辙,因此受控的移動被限制在地面上。垂直運動目前僅是由于重力和浮力朦促。為了控制垂直運動膝晾,我們需要第三個輸入軸。通過將UpDown軸添加到我們的輸入設(shè)置中(通過復(fù)制Horizontal或Vertical)來支持這一點务冕。我將空格(用于跳躍的鍵)用于正鍵血当,將X用作負鍵。然后將playerInput
字段更改為一個Vector3禀忆,并在游泳時將其Z分量設(shè)置為UpDown軸臊旭,否則在Update將其設(shè)置為零。從現(xiàn)在開始箩退,我們必須使用的ClampMagnitude
版本的Vector3
离熏。
Vector3playerInput;
…
void Update () {
playerInput.x = Input.GetAxis("Horizontal");
playerInput.y = Input.GetAxis("Vertical");
playerInput.z = Swimming ? Input.GetAxis("UpDown") : 0f;
playerInput =Vector3.ClampMagnitude(playerInput, 1f);
…
}
找到當(dāng)前和新的Y速度分量,并在AdjustVelocity結(jié)尾用它們調(diào)整速度戴涝。這與X和Z相同滋戳,但僅在游泳時才執(zhí)行。
void AdjustVelocity () {
…
velocity += xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
if (Swimming) {
float currentY = Vector3.Dot(relativeVelocity, upAxis);
float newY = Mathf.MoveTowards(
currentY, playerInput.z * speed, maxSpeedChange
);
velocity += upAxis * (newY - currentY);
}
}
爬和跳
淹沒時應(yīng)該很難爬上或跳下啥刻。我們可以通過在Update中
游泳時忽略玩家的輸入來禁止兩者奸鸯。必須明確取消攀爬的愿望。跳躍會重置自身可帽。如果在下一次更新之前進行了多個物理步驟娄涩,則仍然有可能在游泳時進行攀爬,但這很好映跟,因為在過渡到游泳的過程中會進行攀爬蓄拣,因此準確的時間無關(guān)緊要。要爬出水面努隙,玩家只需在按下爬升按鈕的同時向上游泳球恤,爬升就會在某個時候激活。
if (Swimming) {
desiresClimbing = false;
}
else {
desiredJump |= Input.GetButtonDown("Jump");
desiresClimbing = Input.GetButton("Climb");
}
雖然站在淺水里有跳的可能剃法,但這使它變得困難得多碎捺。我們通過將跳躍速度減小1減去浸沒除以游泳閾值,以最小為零來模擬這一點贷洲。
float jumpSpeed = Mathf.Sqrt(2f * gravity.magnitude * jumpHeight);
if (InWater) {
jumpSpeed *= Mathf.Max(0f, 1f - submergence / swimThreshold);
}
在流水中游泳
在本教程中收厨,我們將不考慮水流,但是我們應(yīng)該處理整個運動的水量优构,因為它們具有動畫效果诵叁,就像我們站立或攀爬的常規(guī)運動幾何一樣。為了使這種可能成為可能钦椭,如果我們結(jié)束游泳拧额,將對撞機傳遞給EvaluateSubmergence并使用其連接的剛體碑诉。如果我們在淺水中,我們將忽略它侥锦。
void OnTriggerEnter (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
EvaluateSubmergence(other);
}
}
void OnTriggerStay (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
EvaluateSubmergence(other);
}
}
void EvaluateSubmergence (Collider collider) {
…
if (Swimming) {
connectedBody = collider.attachedRigidbody;
}
}
如果我們連接到水體进栽,則不應(yīng)用EvaluateCollision中的另一個水體代替它。實際上恭垦,我們根本不需要任何連接信息快毛,因此我們可以在游泳時跳過EvaluateCollision所有工作。
void EvaluateCollision (Collision collision) {
if (Swimming) {
return;
}
…
}
漂浮物
現(xiàn)在我們的球體可以游泳了番挺,如果有一些漂浮的物體可以互動唠帝,那就太好了。再次玄柏,我們必須自己對此進行編程襟衰,方法是將其支持添加到已經(jīng)支持自定義重力的現(xiàn)有組件中。
淹沒
像一樣MovingSphere
粪摘,向CustomGravityRigidbody中添加submergenceOffset 瀑晒,submergenceRange ,buoyancy 赶熟,waterDrag 和 waterMask 瑰妄,除了我們不需要游泳加速度陷嘴,速度或閾值之外映砖。
[SerializeField]
float submergenceOffset = 0.5f;
[SerializeField, Min(0.1f)]
float submergenceRange = 1f;
[SerializeField, Min(0f)]
float buoyancy = 1f;
[SerializeField, Range(0f, 10f)]
float waterDrag = 1f;
[SerializeField]
LayerMask waterMask = 0;
接下來,我們需要一個淹沒字段灾挨。如果需要邑退,在FixedUpdate中施加重力之前將其重置為零。確定淹沒時劳澄,我們還需要知道重力地技,因此也要在野外對其進行跟蹤。
float submergence;
Vector3 gravity;
…
void FixedUpdate () {
…
gravity = CustomGravity.GetGravity(body.position);
if (submergence > 0f) {
submergence = 0f;
}
body.AddForce(gravity, ForceMode.Acceleration);
}
然后添加所需的觸發(fā)方法以及EvaluateSubmergence
方法秒拔,該方法的工作原理與以前相同莫矗,只是我們僅在需要時才計算向上軸,并且不支持連接的物體砂缩。
void OnTriggerEnter (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
EvaluateSubmergence();
}
}
void OnTriggerStay (Collider other) {
if ((waterMask & (1 << other.gameObject.layer)) != 0) {
EvaluateSubmergence();
}
}
void EvaluateSubmergence () {
Vector3 upAxis = -gravity.normalized;
if (Physics.Raycast(
body.position + upAxis * submergenceOffset,
-upAxis, out RaycastHit hit, submergenceRange + 1f,
waterMask, QueryTriggerInteraction.Collide
)) {
submergence = 1f - hit.distance / submergenceRange;
}
else {
submergence = 1f;
}
}
即使漂浮在水中作谚,物體仍然可以進入睡眠狀態(tài)。如果是這種情況庵芭,那么我們可以跳過評估淹沒程度妹懒。因此,如果身體正在睡覺双吆,請不要調(diào)用OnTriggerStay中的?EvaluateSubmergence?眨唬。我們?nèi)匀辉贠nTriggerEnter中這樣做会前,因為這保證了更改。
void OnTriggerStay (Collider other) {
if (
!body.IsSleeping() &&
(waterMask & (1 << other.gameObject.layer)) != 0
) {
EvaluateSubmergence();
}
}
漂浮
在FixedUpdate中匾竿,必要時應(yīng)用水的阻力和浮力。在這種情況下岭妖,我們通過單獨的AddForce
調(diào)用而不是將其與法向重力結(jié)合來應(yīng)用浮力区转。
if (submergence > 0f) {
float drag =
Mathf.Max(0f, 1f - waterDrag * submergence * Time.deltaTime);
body.velocity *= drag;
body.AddForce(
gravity * -(buoyancy * submergence),
ForceMode.Acceleration
);
submergence = 0f;
}
我們還將拖動應(yīng)用于角速度苔巨,以使對象在漂浮時不會保持旋轉(zhuǎn)侄泽。
body.velocity *= drag;
body.angularVelocity *= drag;
浮動對象現(xiàn)在可以在浮動時以任意旋轉(zhuǎn)結(jié)束。通常蜻韭,物體會以最輕的一面朝上的方式漂浮肖方。我們可以通過添加可配置的浮力偏移矢量(默認設(shè)置為零)來模擬這一點俯画。
[SerializeField]
Vector3 buoyancyOffset = Vector3.zero;
然后艰垂,我們通過調(diào)用?AddForceAtPosition
而不是AddForce猜憎,在此時應(yīng)用浮力而不是對象的原點胰柑,并將偏移量轉(zhuǎn)換為單詞空間作為新的第二個參數(shù)柬讨。
body.AddForceAtPosition(
gravity * -(buoyancy * submergence),
transform.TransformPoint(buoyancyOffset),
ForceMode.Acceleration
);
由于重力和浮力現(xiàn)在作用于不同的點谷遂,因此它們會產(chǎn)生角動量卖鲤,從而將偏移點推到頂部。較大的偏移會產(chǎn)生更強的效果窗悯,這會導(dǎo)致快速振蕩蒋院,因此應(yīng)將偏移保持較小。
與浮動對象互動
當(dāng)在其中漂浮著物體的水中游泳時辞友,軌道攝像機會來回晃動,因為它試圖停留在物體的前面鲫尊∫呦颍可以通過添加一個與常規(guī)圖層類似的透視圖層來避免這種情況鸿捧,只是將軌道攝像機設(shè)置為忽略它堆巧。
該層僅應(yīng)用于足夠小以忽略或與之交互的對象啦租。
當(dāng)透視對象遮擋視圖時篷角,我們可以使它們不可見嗎虐块?
是的贺奠,在這種情況下可以檢測到它儡率,可以用來更改對象的可視化。但是箕肃,這不是本教程的一部分勺像。
穩(wěn)定浮動
我們當(dāng)前的方法適用于小型物體,但不適用于較大且不均勻的物體殃姓。例如蜗侈,大的浮動塊在球體與其交互時應(yīng)保持更穩(wěn)定。為了增加穩(wěn)定性该面,我們必須將浮力作用擴展到更大的區(qū)域隔缀。這需要更復(fù)雜的方法,因此CustomGravityRigidbody
將其復(fù)制并重命名為StableFloatingRigidbody
牵触。用偏移矢量數(shù)組替換其浮力偏移敛惊。將浸入也轉(zhuǎn)換為數(shù)組瞧挤,并以Awake
與偏移數(shù)組相同的長度創(chuàng)建它特恬。
public classStableFloatingRigidbody: MonoBehaviour {
…
[SerializeField]
//Vector3 buoyancyOffset = Vector3.zero;
Vector3[] buoyancyOffsets = default;
…
float[]submergence;
Vector3 gravity;
void Awake () {
body = GetComponent<Rigidbody>();
body.useGravity = false;
submergence = new float[buoyancyOffsets.Length];
}
…
}
進行EvaluateSubmergence調(diào)整尝丐,以便分別評估所有浮力偏移量的淹沒度爹袁。
void EvaluateSubmergence () {
Vector3 down = gravity.normalized;
Vector3 offset = down * -submergenceOffset;
for (int i = 0; i < buoyancyOffsets.Length; i++) {
Vector3 p = offset + transform.TransformPoint(buoyancyOffsets[i]);
if (Physics.Raycast(
p,down, out RaycastHit hit, submergenceRange + 1f,
waterMask, QueryTriggerInteraction.Collide
)) {
submergence[i] = 1f - hit.distance / submergenceRange;
}
else {
submergence[i] = 1f;
}
}
}
然后FixedUpdate中
還要對每個偏移量應(yīng)用阻力和浮力譬淳。阻力和浮力都必須除以偏移量,以使最大效果保持不變浦妄。對象所經(jīng)歷的實際效果取決于淹沒的總數(shù)校辩。
void FixedUpdate () {
…
gravity = CustomGravity.GetGravity(body.position);
float dragFactor = waterDrag * Time.deltaTime / buoyancyOffsets.Length;
float buoyancyFactor = -buoyancy / buoyancyOffsets.Length;
for (int i = 0; i < buoyancyOffsets.Length; i++) {
if (submergence[i]> 0f) {
float drag =
Mathf.Max(0f, 1f -dragFactor * submergence[i]);
body.velocity *= drag;
body.angularVelocity *= drag;
body.AddForceAtPosition(
gravity *(buoyancyFactor * submergence[i]),
transform.TransformPoint(buoyancyOffsets[i]),
ForceMode.Acceleration
);
submergence[i]= 0f;
}
}
body.AddForce(gravity, ForceMode.Acceleration);
}
通常把鉴,對于任何盒子形狀庭砍,四個點就足夠了诗轻,除非它們很大或經(jīng)常部分掉出水面扳炬。請注意,偏移量隨對象縮放劝术。同樣养晋,增加對象的質(zhì)量使其更穩(wěn)定。
意外的懸浮
如果一個點最終在表面上方足夠高,則其光線投射將失敗蛾娶,這將使其錯誤地算作完全淹沒蛔琅。對于具有多個浮點的大型物體來說,這是一個潛在的問題寨躁,因為有些物體可能最終落在水面之上所禀,而物體的另一部分仍被淹沒。結(jié)果將是高峰最終浮空褂策。您可以通過將一個較大的輕物體部分地從水中推出來實現(xiàn)此目的斤寂。
該問題仍然存在,因為部分物體仍然接觸水。為了解決這個問題再愈,當(dāng)射線投射無法檢查該點本身是否在水量之內(nèi)時,我們必須執(zhí)行一個額外的查詢抗悍。可以通過調(diào)用Physics.CheckSphere
位置和小半徑(例如0.01)作為參數(shù)衔沼,然后調(diào)用遮罩和交互模式來完成此操作。僅當(dāng)該查詢返回時true
凝化,我們才應(yīng)將淹沒設(shè)置為1劣光。但是牲剃,這可能會導(dǎo)致大量額外的查詢凿傅,因此辨液,通過添加可配置的安全浮動切換項,使其變?yōu)榭蛇x燎悍。僅對于可以充分推入水中的大型物體才需要。
[SerializeField]
bool safeFloating = false;
…
void EvaluateSubmergence () {
Vector3 down = gravity.normalized;
Vector3 offset = down * -submergenceOffset;
for (int i = 0; i < buoyancyOffsets.Length; i++) {
Vector3 p = offset + transform.TransformPoint(buoyancyOffsets[i]);
if (Physics.Raycast(
p, down, out RaycastHit hit, submergenceRange + 1f,
waterMask, QueryTriggerInteraction.Collide
)) {
submergence[i] = 1f - hit.distance / submergenceRange;
}
elseif (
!safeFloating || Physics.CheckSphere(
p, 0.01f, waterMask, QueryTriggerInteraction.Collide
)
){
submergence[i] = 1f;
}
}
}
下一個教程是互動環(huán)境臊诊。
資源庫(Repository)
https://bitbucket.org/catlikecodingunitytutorials/movement-09-swimming/
往期精選
Unity3D游戲開發(fā)中100+效果的實現(xiàn)和源碼大全 - 收藏起來肯定用得著
Shader學(xué)習(xí)應(yīng)該如何切入潜叛?
聲明:發(fā)布此文是出于傳遞更多知識以供交流學(xué)習(xí)之目的。若有來源標注錯誤或侵犯了您的合法權(quán)益椒舵,請作者持權(quán)屬證明與我們聯(lián)系犁钟,我們將及時更正涝动、刪除,謝謝米愿。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/movement/swimming/
翻譯、編輯、整理:MarsZhou
More:【微信公眾號】?u3dnotes