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

Unity多人贪吃蛇项目:蛇身朝向匹配预制件问题求助

问题描述
  • 正在开发趣味多人贪吃蛇项目,Unity中已创建两个蛇身预制件:SnakeBodyHorizontal(水平蛇身)、SnakeBodyVertical(垂直蛇身)
  • 需求:蛇身根据移动方向切换预制件——上下移动时使用水平预制件,左右移动时使用垂直预制件,默认蛇身为水平状态
  • 当前问题:无法让所有蛇身段始终保持正确朝向(如图所示),曾尝试通过前后段X值是否相同来旋转预制件,但存在严重bug,急需更优解决方案

蛇身朝向错误示例

当前代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Snake : MonoBehaviour
{

    private Vector2 _direction = Vector2.right;
    private List<Transform> _segments = new List<Transform>();
    private KeyCode lastHitKey = KeyCode.D;

    public Transform segmentPrefab;
    public int initialSize = 3;

    //sprites for snake head 
    public Sprite SnakeHeadUp;
    public Sprite SnakeHeadRight;
    public Sprite SnakeHeadLeft;
    public Sprite SnakeHeadDown;

    private void Start()
    {
        ResetState();
    }


    // Update: Updated Once Per Time (Edit -> Project Settings -> Time (0.09) to change difficulty!)
    // This function is controlling the direction our snake moves.
    void Update()
    {

        if(Input.GetKeyDown(KeyCode.W) && lastHitKey != KeyCode.S)
        {
            this.gameObject.GetComponent<SpriteRenderer>().sprite = SnakeHeadUp;
            lastHitKey = KeyCode.W;
            _direction = Vector2.up; 

        } else if (Input.GetKeyDown(KeyCode.S) && lastHitKey != KeyCode.W)
        {
            this.gameObject.GetComponent<SpriteRenderer>().sprite = SnakeHeadDown;
            lastHitKey = KeyCode.S;
            _direction = Vector2.down;
            
        } else if (Input.GetKeyDown(KeyCode.A) && lastHitKey != KeyCode.D)
        {
            this.gameObject.GetComponent<SpriteRenderer>().sprite = SnakeHeadLeft;
            lastHitKey = KeyCode.A;
            _direction = Vector2.left;

        } else if(Input.GetKeyDown(KeyCode.D) && lastHitKey != KeyCode.A)
        {
            this.gameObject.GetComponent<SpriteRenderer>().sprite = SnakeHeadRight;
            lastHitKey = KeyCode.D;
            _direction = Vector2.right;
        }
    }

    //This transforms the position of our snake head, rounded to a whole number. This should force our snake to move in a grid.
    //if a segment has the same x value as the previous segment, it must be cos turnt!!! then change segments sprite
    public void FixedUpdate()
    {

        for (int i = _segments.Count - 1; i > 0; i--){ 
            _segments[i].position = _segments[i - 1].position;
        }

        this.transform.position = new Vector3(
            Mathf.Round(this.transform.position.x) + _direction.x,
            Mathf.Round(this.transform.position.y) + _direction.y,
            0.0f
        );  
    }

    //Function handles the logic of iterating in reverse from a list of prefabs of our snake segments, to create a new snake segment.
    private void Grow()
    {
        Transform segment = Instantiate(this.segmentPrefab);
        segment.position = _segments[_segments.Count - 1].position; //each segment inherits position

        _segments.Add(segment);
    }

    //Function handles the logic of resetting the state of our game upon collision with objects other than food
    private void ResetState(){

        //emptying the snakelist
        for(int i = 1; i < _segments.Count; i++)
        {
            Destroy(_segments[i].gameObject);
        }

        _segments.Clear();
        _segments.Add(this.transform);

        //resetting our snake from 1 back to initialsize (3)
        for(int i = 1; i < this.initialSize; i++)
        {
            _segments.Add(Instantiate(this.segmentPrefab));
            //initalizing the original segments
            _segments[i].position = new Vector3(
            -50.0f,
            0.0f,
            0.0f
        );
        }

        this.transform.position = new Vector3(
            -5.0f,
            0.0f,
            0.0f
        );
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Food")
        {
            Grow();
        }
        else if (other.tag == "Obstacle")
        {
            ResetState();
        }
    }
}
解决方案

核心思路:放弃通过位置判断方向的逻辑,改为给每个蛇身段记录自身相对于前一段的移动方向,以此准确切换水平/垂直预制件(或Sprite)。

1. 新增方向列表维护段的方向

在Snake脚本中添加一个与_segments对应的方向列表,存储每个段相对于前一段的方向:

private List<Vector2> _segmentDirections = new List<Vector2>();

2. 同步更新位置与方向

修改FixedUpdate方法,在移动蛇身段时,同步记录每个段的方向:

// 在脚本顶部添加两个Sprite变量,用于赋值水平/垂直蛇身
public Sprite horizontalBodySprite;
public Sprite verticalBodySprite;

public void FixedUpdate()
{
    // 记录头部移动前的位置,用于计算头部方向
    Vector3 headOldPos = transform.position;
    
    // 更新头部位置
    transform.position = new Vector3(
        Mathf.Round(headOldPos.x) + _direction.x,
        Mathf.Round(headOldPos.y) + _direction.y,
        0.0f
    );
    // 更新头部的方向(就是当前移动方向)
    _segmentDirections[0] = _direction;

    // 从后往前更新所有身体段的位置和方向
    for (int i = _segments.Count - 1; i > 0; i--)
    {
        Vector3 segmentOldPos = _segments[i].position;
        // 更新当前段位置为前一段的旧位置
        _segments[i].position = _segments[i - 1].position;
        // 计算当前段相对于前一段的方向(前一段旧位置到当前段旧位置的反向)
        _segmentDirections[i] = (segmentOldPos - _segments[i].position).normalized;
    }

    // 遍历身体段,根据方向切换Sprite
    for (int i = 1; i < _segments.Count; i++)
    {
        SpriteRenderer sr = _segments[i].GetComponent<SpriteRenderer>();
        Vector2 dir = _segmentDirections[i];
        if (dir == Vector2.left || dir == Vector2.right)
        {
            // 左右方向:蛇身垂直排列,使用垂直预制件的Sprite
            sr.sprite = verticalBodySprite;
        }
        else
        {
            // 上下方向:蛇身水平排列,使用水平预制件的Sprite
            sr.sprite = horizontalBodySprite;
        }
    }
}

3. 初始化与生长逻辑适配

修改ResetState和Grow方法,同步初始化方向列表:

private void ResetState(){
    // 先销毁旧段
    for(int i = 1; i < _segments.Count; i++)
    {
        Destroy(_segments[i].gameObject);
    }

    _segments.Clear();
    _segmentDirections.Clear();
    
    _segments.Add(this.transform);
    _segmentDirections.Add(_direction); // 头部初始方向为right

    // 初始化初始长度的蛇身
    for(int i = 1; i < this.initialSize; i++)
    {
        Transform segment = Instantiate(this.segmentPrefab);
        segment.position = new Vector3(-50.0f, 0.0f, 0.0f);
        _segments.Add(segment);
        // 初始段方向默认与头部一致(水平)
        _segmentDirections.Add(Vector2.right);
    }

    this.transform.position = new Vector3(-5.0f, 0.0f, 0.0f);
}

private void Grow()
{
    Transform segment = Instantiate(this.segmentPrefab);
    segment.position = _segments[_segments.Count - 1].position;
    _segments.Add(segment);
    // 新增段的方向与最后一个段方向一致
    _segmentDirections.Add(_segmentDirections[_segmentDirections.Count - 1]);
}

优化说明

  • 用方向列表替代位置判断:彻底避免位置重叠、未移动等情况导致的判断错误
  • 切换Sprite替代预制件替换:比销毁再实例化预制件性能更高,逻辑更稳定
  • 方向与段一一绑定:每个段的方向信息实时同步,确保朝向准确

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 09:38:10