【游戏开发实战】Unity 2D游戏物理运动曲线轨迹预测,以愤怒的小鸟为例,轨迹曲线云团圈圈

本文详细介绍了如何在Unity中实现类似愤怒的小鸟的游戏场景,通过预测物理运动轨迹并在游戏开始前显示,增强玩家体验。首先分析了斜抛运动的物理公式,然后逐步讲解了场景搭建、关键代码实现和脚本挂载过程,最后展示了运行效果。提供的源代码可供读者下载学习。
摘要由CSDN通过智能技术生成

一、前言

点关注不迷路,持续输出Unity干货文章。

嗨,大家好,我是新发。我们先来看一个画面,愤怒的小鸟,发射之前没用轨迹预测。
在这里插入图片描述
我们可以不可以在发射之前就预测出曲线轨迹并显示出来呢?
本文就来教大家如何在Unity中实现物理运动曲线预测吧。

本文最终效果:
在这里插入图片描述
本文Demo工程已上传到CodeChina,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/UnityAngryBirdCurveTrajectory
注:我使用的Unity版本:2020.2.7f1c1 (64-bit)
在这里插入图片描述

二、思考分析

根据高中物理的斜抛运动路径公式:
水平方向: s x = v x t s_x=v_xt sx=vxt
竖直方向: s y = v y t − ( 1 / 2 ) g t 2 s_y=v_yt-(1/2)gt^2 sy=vyt(1/2)gt2
我们只要知道 v x v_x vx v y v_y vy即可预测出曲线的轨迹,而 ( v x , v y ) (v_x, v_y) (vx,vy)就是速度向量。
那么,我们怎么知道 ( v x , v y ) (v_x, v_y) (vx,vy)呢?
手指按下的位置作为起始点,手指抬起的位置作为结束点,起始点 - 结束点即可得到一个从结束点指向起始点的向量,这个就是我们要的速度向量 ( v x , v y ) (v_x, v_y) (vx,vy)
在这里插入图片描述
现在我们翻译成代码,先定义些必要的变量:

/// <summary>
/// 手指的起始点
/// </summary>
private Vector2 m_startPoint;
/// <summary>
/// 手指的结束点
/// </summary>
private Vector2 m_endPoint;
/// <summary>
/// 起始点和结束点的距离
/// </summary>
private float m_distance;
/// <summary>
/// 方向向量,从结束点指向起始点的归一化向量
/// </summary>
private Vector2 m_direction;

起始点和结束点的坐标,可以通过手指的屏幕坐标转世界坐标得到,例:

// 起始点坐标
m_startPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);
// ...

// 结束点坐标
m_endPoint = Camera.main.ScreenToWorldPoint(Input.mousePosition);

手指抬起的时候,计算速度向量:

// 放大速度倍数
float factor = 4f;

m_distance = Vector2.Distance(m_startPoint, m_endPoint);
m_direction = (m_startPoint - m_endPoint).normalized;
Vector2 speed = m_direction * m_distance * factor;

有了这个speed,我们就可以预测轨迹了。
假设鸟的坐标为Vector3 birdPos,根据斜抛路径公式,那么预测曲线轨迹点的坐标(posX, posY)就是这样:

float posX = birdPos.x + speed.x * t;
float posY = birdPos.x + speed.y * t - 0.5f * Physics2D.gravity.magnitude * t * t;

注意,上面我们用了Physics2D.gravity.magnitude这个值,它就是重力加速度g
它的值可以在Project Settings中设置:
在这里插入图片描述
另外,我们需要让鸟根据初始的speed做斜抛运动,这里要用到Rigidbody2DAddForce接口,例:

rigidbody2D.AddForce(speed, ForceMode2D.Impulse);

好了,思考完毕,开始动手实际操作吧。

三、场景搭建

1、导入图片素材

为了方便演示,我们先找点图片素材搭建场景,素材图片:
请添加图片描述
请添加图片描述请添加图片描述请添加图片描述

2、鸟预设

先做个鸟预设,
在这里插入图片描述
身上挂上必要的物理组件,
在这里插入图片描述
为了让鸟弹到地面上之后有一个弹性碰撞,我们创建一个Physics Material 2D,
在这里插入图片描述
重名名为bounciness(这个图标很形象呀)
在这里插入图片描述
设置摩擦力和弹力分别为0.50.6
在这里插入图片描述

3、地面环境

做个地面预设,左右两边放两个带碰撞体的石柱,防止鸟飞出屏幕外。
在这里插入图片描述
地面添加两个碰撞体,不然鸟会掉到屏幕外。
在这里插入图片描述

4、曲线的点预设

为了描绘曲线,我们用这个小云团作为一个个点,
请添加图片描述
将其做成预设,只需要基本的SpriteRenderer组件即可。
在这里插入图片描述

5、预览效果

运行Unity,预览下效果,可以看到,我们的鸟已经具备物理特性了。
在这里插入图片描述
接下来就是写代码实现我们的业务逻辑了。

四、代码

撸起袖子写代码,代码如下,注释我写得比较清楚,就不赘述了。

1、鸟脚本:Bird.cs
// Bird.cs

using UnityEngine;

/// <summary>
/// 鸟
/// </summary>
public class Bird : MonoBehaviour
{
    [HideInInspector] public Rigidbody2D rb;
    [HideInInspector] public CircleCollider2D col;
    [HideInInspector] public Vector3 pos
    {
        get { return transform.position; }
    }

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        col = GetComponent<CircleCollider2D>();
    }

    /// <summary>
    /// 给鸟一个推力,将鸟推出去
    /// </summary>
    /// <param name="speed">速度向量</param>
    public void Push(Vector2 speed)
    {
        rb.AddForce(speed, ForceMode2D.Impulse);
    }

    /// <summary>
    /// 激活物理
    /// </summary>
    public void ActivateRb()
    {
        rb.isKinematic = false;
    }

    /// <summary>
    /// 禁用物理
    /// </summary>
    public void DesActivateRb()
    {
        rb.velocity = Vector3.zero;
        rb.angularVelocity = 0f;
        rb.isKinematic = true;
    }
}
2、曲线预测器:Trajectory.cs
// Trajectory.cs

using UnityEngine;

/// <summary>
/// 曲线预测器
/// </summary>
public class Trajectory : MonoBehaviour
{
    /// <summary>
    /// 预测点的数量
    /// </summary>
    [SerializeField] private int m_dotsNum = 20;
    /// <summary>
    /// 点物体的父节点
    /// </summary>
    [SerializeField] private GameObject m_dotsParent;
    /// <summary>
    /// 点预设
    /// </summary>
    [SerializeField] private GameObject m_dotsPrefab;
    /// <summary>
    /// 点间距
    /// </summary>
    [SerializeField] private float m_dotSpacing = 0.01f;
    /// <summary>
    /// 点的最小缩放
    /// </summary>
    [SerializeField] [Range(0.01f, 0.3f)] private float m_dotMinScale = 0.1f;
    /// <summary>
    /// 点的最大缩放
    /// </summary>
    [SerializeField] [Range(0.3f, 1f)] private float m_dotMaxScale = 1f;


    private Transform[] m_dotsList;
    private Vector2 m_pos;
    private float m_timeStamp;

    private void Start()
    {
        Hide();
        PrepareDots();
    }

    /// <summary>
    /// 准备轨迹点
    /// </summary>
    private void PrepareDots()
    {
        m_dotsList = new Transform[m_dotsNum];
        m_dotsPrefab.transform.localScale = Vector3.one * m_dotMaxScale;
        float scale = m_dotMaxScale;
        float scaleFactor = scale / m_dotsNum;

        for (int i = 0; i < m_dotsNum; ++i)
        {
            var dot = Instantiate(m_dotsPrefab).transform;
            dot.parent = m_dotsParent.transform;
            dot.localScale = Vector3.one * scale;
            if (scale > m_dotMinScale)
                scale -= scaleFactor;
            m_dotsList[i] = dot;
        }
    }

    /// <summary>
    /// 更新点坐标
    /// </summary>
    /// <param name="birdPos">鸟的坐标</param>
    /// <param name="pushSpeed">初始速度向量</param>
    public void UpdateDots(Vector2 birdPos, Vector2 pushSpeed)
    {
        m_timeStamp = m_dotSpacing;
        
        for (int i = 0; i < m_dotsNum; ++i)
        {
            m_pos.x = birdPos.x + pushSpeed.x * m_timeStamp;
            m_pos.y = (birdPos.y + pushSpeed.y * m_timeStamp) - 0.5f * Physics2D.gravity.magnitude * m_timeStamp * m_timeStamp;
            m_dotsList[i].position = m_pos;
            m_timeStamp += m_dotSpacing;
        }
    }

    /// <summary>
    /// 显示预测轨迹
    /// </summary>
    public void Show()
    {
        m_dotsParent.SetActive(true);
    }

    /// <summary>
    /// 隐藏预测轨迹
    /// </summary>
    public void Hide()
    {
        m_dotsParent.SetActive(false);
    }
}
3、游戏管理器:GameManager.cs
// GameManager.cs
using UnityEngine;

/// <summary>
/// 游戏管理器
/// </summary>
public class GameManager : MonoBehaviour
{
    /// <summary>
    /// 鸟
    /// </summary>
    public Bird bird;
    /// <summary>
    /// 轨迹预测器
    /// </summary>
    public Trajectory trajectory;

    /// <summary>
    /// 主摄像机
    /// </summary>
    private Camera m_cam;

    /// <summary>
    /// 力大小
    /// </summary>
    [SerializeField]
    private float m_speedFactor = 4f;

    /// <summary>
    /// 是否拉动中
    /// </summary>
    private bool m_isDragging = false;
    /// <summary>
    /// 手指的起始点
    /// </summary>
    private Vector2 m_startPoint;
    /// <summary>
    /// 手指的结束点
    /// </summary>
    private Vector2 m_endPoint;
    /// <summary>
    /// 起始点和结束点的距离
    /// </summary>
    private float m_distance;
    /// <summary>
    /// 方向向量,从结束点指向起始点的归一化向量
    /// </summary>
    private Vector2 m_direction;
    /// <summary>
    /// 力向量
    /// </summary>
    private Vector2 m_pushSpeed;


    private void Start()
    {
        m_cam = Camera.main;
        bird.DesActivateRb();
    }

    private void Update()
    {
        // 检测鼠标/手指行为
        if(Input.GetMouseButtonDown(0))
        {
            m_isDragging = true;
            OnDragStart();
        }
        if (Input.GetMouseButtonUp(0))
        {
            m_isDragging = false;
            OnDragEnd();
        }

        if (m_isDragging)
        {
            OnDrag();
        }
    }

    /// <summary>
    /// 开始拉
    /// </summary>
    private void OnDragStart()
    {
        // 禁用物理
        bird.DesActivateRb();
        // 起始点
        m_startPoint = m_cam.ScreenToWorldPoint(Input.mousePosition);
        // 显示轨迹
        trajectory.Show();
    }

    /// <summary>
    /// 拉中
    /// </summary>
    private void OnDrag()
    {
        m_endPoint = m_cam.ScreenToWorldPoint(Input.mousePosition);
        m_distance = Vector2.Distance(m_startPoint, m_endPoint);
        m_direction = (m_startPoint - m_endPoint).normalized;
        m_pushSpeed = m_direction * m_distance * m_speedFactor;

        trajectory.UpdateDots(bird.pos, m_pushSpeed);
    }

    /// <summary>
    /// 拉结束
    /// </summary>
    private void OnDragEnd()
    {
        bird.ActivateRb();
        bird.Push(m_pushSpeed);
        // 隐藏轨迹
        trajectory.Hide();
    }
}

五、挂脚本

鸟预设上挂Bird脚本。
在这里插入图片描述
曲线预测器挂Trajectory脚本,并赋值对应的参数。
在这里插入图片描述
游戏管理器挂GameManager脚本,并赋值对应的参数。
在这里插入图片描述

六、运行测试

运行Unity,测试效果如下,可以看到,飞行轨迹与我们的预测的曲线轨迹一致。
在这里插入图片描述
完毕。
喜欢Unity的同学,不要忘记点击关注,如果有什么Unity相关的技术难题,也欢迎留言或私信~

  • 25
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林新发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值