You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity如何让物体朝向移动方向 仅XZ轴旋转避免角色穿地

Unity角色转向穿地问题修复

问题现象

  • 实现角色朝向移动方向逻辑时,挂载/移除Rigidbody测试均出现异常:角色初始转向正常,到达目标塔举盾时预制体异常旋转、下陷穿地
  • 需求:角色仅在X、Z轴平面完成转向,全程锁定Y轴旋转角度,不产生倾斜
  • 附加线索:预制体实例化时子物体位置数值异常,移除Rigidbody后问题未消失

问题根因

问题和Rigidbody无直接关联,核心是代码逻辑存在4个错误:

  • 位置锁定逻辑写错:Update开头的位置修正代码同时强制设置pos.x=0、pos.y=0,会直接把角色拉到世界坐标X=0、Y=0的位置,完全不是锁定Y轴高度的逻辑
  • 朝向计算未做水平约束:直接将带Y轴分量的方向向量赋值给transform.forward,只要目标和角色存在高度差、动画带Y轴偏移、碰撞产生微小Y位移,就会让角色朝向带上俯仰/倾斜角度,触发X、Z轴旋转,最终歪倒穿地
  • 攻击状态未做朝向锁定:角色停步攻击时没有持续约束旋转,动画根运动、碰撞抖动都可能带偏角色角度
  • 缺少空值防护:场景内无Block标签物体时,getClosestBlock()返回null,后续直接访问该对象属性会触发报错、中断逻辑

修复方案

修正后完整代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlockRadar : MonoBehaviour
{
    public Transform Tower;

    private GameObject[] multipleBlocks;
    public Transform closestBlock;
    public bool blockContact;
    
    public float currentDistance;
    public float stopDistance;

    public Animator animator;
    public int damage;
    public float attackspeed;
    private float canAttack;

    public float moveSpeed = 5f;
    private Vector3 movement;
    // 缓存角色初始地面高度,用于锁定Y轴位置
    private float _initGroundY;

    void Start()
    {
        closestBlock = null;
        blockContact = false;
        // 开局记录角色所在地面的Y轴坐标
        _initGroundY = transform.position.y;
    }

    void Update()
    {
        // 仅锁定Y轴高度,不修改X、Z轴的移动坐标
        Vector3 pos = transform.position;
        pos.y = _initGroundY;
        transform.position = pos;
        // 每帧强制清空X、Z轴旋转,从根源避免倾斜
        Vector3 rot = transform.eulerAngles;
        rot.x = 0;
        rot.z = 0;
        transform.eulerAngles = rot;

        closestBlock = getClosestBlock();
        // 无目标时直接返回,避免空引用报错
        if (closestBlock == null)
        {
            animator.SetBool("Moving", false);
            return;
        }
        closestBlock.gameObject.GetComponent<Renderer>().material.color = new Color(1, 0.7f, 0, 1);

        // 计算方向时强制清空Y轴分量,保证方向完全处于XZ水平面
        Vector3 direction = closestBlock.position - transform.position;
        direction.y = 0;
        direction.Normalize();
        movement = direction;

        float dist = Vector3.Distance(closestBlock.position, transform.position);
        if (dist <= 1.5f)
        {
            blockContact = true;
            animator.SetBool("Moving", false);
            // 攻击状态下也保持朝向目标,避免角度偏移
            transform.forward = movement;
            if (attackspeed <= canAttack)
            {
                Attack();
                canAttack = 0f;
            }
            else
            {
                canAttack += Time.deltaTime;
            }
        }
        else
        {
            transform.forward = movement;
            blockContact = false;
            animator.SetBool("Moving", true);
            moveCharacter(movement);
        }
    }

    public void Attack()
    {             
        Debug.Log("ATTACKING!");
        Damage(closestBlock.transform);
        animator.SetTrigger("Attacking");       
    }

    void moveCharacter(Vector3 direction)
    {
        transform.Translate(direction * moveSpeed * Time.deltaTime, Space.World);
    }

    private void OnDestroy()
    {
        if (closestBlock !=null)
        {
            closestBlock.gameObject.GetComponent<Renderer>().material.color = new Color(0, 0, 0, 0);
        }
    }

    public Transform getClosestBlock()
    {
        multipleBlocks = GameObject.FindGameObjectsWithTag("Block");
        float closestDistance = Mathf.Infinity;
        Transform trans = null;

        foreach (GameObject go in multipleBlocks)
        {
            currentDistance = Vector3.Distance(transform.position, go.transform.position);
            if (currentDistance < closestDistance)
            {
                closestDistance = currentDistance;
                trans = go.transform;
            }
        }
        return trans;
    }

    void Damage(Transform block)
    {
        Tower_Stats e = block.GetComponent<Tower_Stats>();
        if (e != null)
        {
            e.TakeDamage(damage);
        }
    }
}

配套配置调整

  • 如果保留Rigidbody组件,在Rigidbody的Constraints栏勾选Freeze Position Y、Freeze Rotation X、Freeze Rotation Z,从物理层面双重锁定倾斜和下陷
  • 如果开启了Animator的Apply Root Motion,要么关闭该选项用代码完全控制位移旋转,要么进入举盾动画的剪辑面板,删除根运动轨道上的X/Z旋转、Y轴位移关键帧,避免动画带动角色偏移
  • 预制体子物体位置异常排查:修复根节点约束后如果问题仍存在,检查子物体碰撞体是否和角色自身碰撞体产生自碰撞,将角色所有部件放到同一层,在Physics设置里开启该层的层内碰撞忽略即可;如果是骨骼节点坐标异常,重新配置Avatar的骨骼映射,修正绑定错误

内容的提问来源于stack exchange,提问作者E C

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 16:39:19