Unity3D基于Rigidbody实现玩家堆叠方块随斜坡跳跃方案
问题背景
我正在开发一款玩法与Matching Cubes高度相似的超休闲游戏:
- 最初实现圆柱玩家下方堆叠方块的逻辑为直接修改
transform属性,但游戏内存在需要跳跃通过的斜坡路段,直接修改坐标会绕过物理模拟,导致对象直接穿过斜坡。 - 后续为圆柱玩家挂载
Rigidbody接入物理系统,无堆叠方块时玩家可正常完成斜坡跳跃,但需要实现携带所有堆叠方块一同完成跳跃的效果。尝试过MovePosition、AddForce等API,均无法在Rigidbody模式下实现方块稳定堆叠。 - 曾尝试平路阶段用
transform完成定位,碰撞到斜坡时停止调用定位逻辑并施加AddForce(Vector3.up*offset)向上力,但对象完全没有产生向上位移。
当前核心定位逻辑集中在StackManager.cs脚本的Regulator()方法中,该方法会遍历picks列表调整所有堆叠对象的位置,完整脚本如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Threading.Tasks; public class StackManager : MonoBehaviour { public static StackManager instance; [SerializeField] private float distanceBetweenObjs; [SerializeField] private Transform prevObject = null; [SerializeField] private Transform parent; [SerializeField] private Transform cylinder; [SerializeField] private Transform trailer; private List<Transform> picks; // Use this to order gate and random gate private Vector3 firstPosition; private int comboCounter; // fever mode tracker private Rigidbody rb; private Transform prev; private void Awake() { if(instance == null) { instance = this; } } void Start() { rb = cylinder.gameObject.GetComponent<PlayerController>().rb; comboCounter = 0; picks = new List<Transform>(); firstPosition = new Vector3(cylinder.position.x, cylinder.position.y, cylinder.position.z); } // Update is called once per frame void Update() { Regulator(); } void CheckChildren() { List<Transform> children = new List<Transform>(); foreach (Transform child in picks) { children.Add(child); } for(int i = 0; i < children.Count - 2; i++) { if (children[i].isSameMaterial2(children[i+1], children[i+2])) { comboCounter++; Destroy(children[i].gameObject); Destroy(children[i+1].gameObject); Destroy(children[i+2].gameObject); picks.Remove(children[i]); picks.Remove(children[i+1]); picks.Remove(children[i+2]); }; } if(comboCounter == 3) { SpeedBoost(); //fever mode comboCounter = 0; } } public void PickUp(GameObject pickedObj){ pickedObj.tag = "Picked"; pickedObj.transform.parent = parent; picks.Add(pickedObj.transform); } private void Regulator(){ //Position of cylinder //set y value based on the # of children objects /** *hold the first position of cylinder *make calculation by referencing it *reference + localScale.y + 0.1f: */ Vector3 newPos = new Vector3(cylinder.position.x, firstPosition.y, cylinder.position.z); foreach (Transform child in picks) { newPos.y += child.localScale.y + 0.1f; } //cylinder.position = newPos; rb.MovePosition(newPos); //Position of children if(picks.Count>0) { prevObject = picks[picks.Count-1]; } /** *For each child * cylinder-0.1f-pick-0.1f-pick-... */ prev = cylinder; for(int i = 0; i < picks.Count; i++) { if(i==0){ picks[i].position = new Vector3(prev.position.x, prev.position.y-1.2f, prev.position.z); //picks[i].gameObject.GetComponent<Rigidbody>().position(new Vector3(prev.position.x, prev.position.y-1.2f, prev.position.z)); } else { //picks[i].gameObject.GetComponent<Rigidbody>().MovePosition(new Vector3(prev.position.x, prev.position.y-prev.localScale.y -0.1f, prev.position.z)); picks[i].position = new Vector3(prev.position.x, prev.position.y-prev.localScale.y -0.1f, prev.position.z); } prev = picks[i]; } //Position of trailer object if(picks.Count>0) { trailer.position = new Vector3(prev.position.x, prev.position.y-0.2f, prev.position.z); //relocate the trailer object under last pick trailer.GetComponent<TrailRenderer>().material = prev.GetComponent<MeshRenderer>().material; //change the color of the trail } CheckChildren(); //check for the 3-conjugate combo } public void ChangeRampState() { Debug.Log("prev.gameObject"); Debug.Log(prev.gameObject); prev.gameObject.GetComponent<Collider>().isTrigger = false; rb.AddForce(Vector3.up * 300); } public void OrderPicks() { picks.Sort((x, y) => string.Compare(x.GetComponent<MeshRenderer>().sharedMaterial.name, y.GetComponent<MeshRenderer>().sharedMaterial.name)); } public void ShufflePicks() { picks = picks.Fisher_Yates_CardDeck_Shuffle(); } async public void onObstacle(Transform pickToDestroy) { pickToDestroy.gameObject.GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezePosition; pickToDestroy.parent = null; await Task.Delay(200); picks.Remove(pickToDestroy); } public void SpeedBoost() { cylinder.gameObject.GetComponent<PlayerController>().rb.velocity *= 2; } }
实现方案
核心问题是每帧强制修改对象坐标/调用MovePosition硬拉位置的逻辑,本质和直接改transform没有区别,会打断物理引擎的连续模拟,堆叠对象之间没有稳定的力传导路径,自然会出现穿模、受力无响应、堆叠散架的问题,按以下步骤调整即可实现稳定效果:
- 统一物理参数配置:所有可堆叠方块都挂载
Rigidbody,碰撞检测模式设为Continuous Dynamic防止高速移动穿模,插值模式设为Interpolate保证移动平滑,质量按比例设置(比如玩家圆柱刚体质量设为10,每个方块质量设为1),避免堆叠结构头重脚轻易翻倒。 - 用可配置关节替代硬坐标设置:拾取新方块时,给方块添加
ConfigurableJoint,将关节的连接目标设为它正上方的对象(最顶部方块连接玩家圆柱,下方方块依次连接上一层方块),锁定所有旋转轴自由度,把XYZ轴的位置驱动弹性系数调高、阻尼拉满,让方块稳定保持在上方对象的正下方固定偏移位置,同时保留完整物理碰撞响应,碰到斜坡时会自动被地面托举,不会穿模。 - 调整定位逻辑:删掉
Regulator()方法里每帧直接给picks[i].position赋值、调用rb.MovePosition硬拉玩家位置的代码,只保留方块排序、连击检测、尾迹材质更新的逻辑。每帧只需要给每个关节更新目标位置为上方对象正下方的固定偏移即可,不要直接干预刚体坐标。 - 斜坡/跳跃逻辑适配:玩家跳跃、上斜坡时不需要单独给方块施加作用力,玩家受到的跳跃力、斜坡的支撑力会通过关节自动传导到整个堆叠结构,所有方块会跟随玩家同步运动,不会散架。之前调用
AddForce没有效果,就是因为力施加后立刻被每帧的坐标重置逻辑抵消了,删掉硬改坐标的部分后力的响应会恢复正常。 - 碰撞处理优化:方块碰到障碍物需要掉落时,直接销毁对应方块的关节组件,取消位置约束,方块就会自动受重力掉落,不需要额外加位置冻结逻辑。
内容的提问来源于stack exchange,提问作者MUSA ZENBİLCİ
相关产品推荐
相关产品推荐

