Unity实现人物移动、旋转、跳跃解决方案(移动端)

3,318 阅读5分钟

前言

自写文章《 Unity实现人物移动、旋转、跳跃解决方案(电脑端)》已经过了好长时间了,今天给大家带来移动端的教学!起初我在网上也找了很多实现的方法,基本都是通过Touch类来实现,但是跟着写代码后都很难实现我想要的流畅效果,后面只能自己含泪去研究😭,不过最后还是实现了,在这篇文章中我会将过程详细的描述出来,带着大家一起去实现类似cf手游的人物控制系统。

实现效果

这里左边摇杆操控操控移动,右边的按钮控制跳跃,其余空白部分操控视角。

20221013_143622.gif

实现流程

这里我继续使用当时做电脑端时搭的模型,角色的创建流程和环境的搭建都是一样的,新来的小伙伴可以先去上一篇文章学习一下。

设计触控面板

1、点开人物预设体,在预设体中创建一个画布,这里我命名为“PlayerCanvas”,于Camera同层级。

image.png

2、把视角切换成2D,调整好画布位置,方便我们操作。将“PlayerCanvas”的UI Scale Mode设置成Scale With Screen Size,这样摇杆和跳跃按钮才可以跟着屏幕的大小适配。

image.png

3、在“PlayerCanvas”中创建Panel面板,命名为“MovePanel”。在“MovePanel”中添加一张图片和一个按钮,图片命名为“JoySliceOut”,是摇杆的外框,按钮命名为“Jump”,是跳跃按钮。将图片锚点和中心点设置在“MovePanel”在左下角,按钮的锚点和中心点设置在“MovePanel”的右下角。快速定位锚点和中心点可以点击每个需要定位的UI中的Rect Transform,通过Alt和Shift同时按下选择

image.png

image.png

4、设置“JoySliceOut”和“Jump”的宽高,这里需要注意一下:因为Canvas模式设置成了Scale With Screen Size,所以调节宽高尽量不要直接使用拖拉和缩放的方式,应该通过数据面板上Rect Transform中的Width和Height来修改。这里“JoySliceOut”我设置成了 140 x 140 并添加了Sprite背景,“Jump”设置成了 70 x 70,去除了子组件Text中的文字,也添加了Sprite背景。最后将两个组件调节好合适的位置(可通过拖拉的方式)。

image.png

5、在“JoySliceOut”中添加图片,命名为“JoySlice”,这个是摇杆的内芯,设置大小 70 x 70 ,添加摇杆内芯的Sprite背景图。

image.png

6、去除“MovePanel”的背景色,将透明度调成0就可以了。至此,触控面板就做好了。

image.png

功能设计思路

1、摇杆带动人物移动,跳跃按钮实现人物跳跃,其余空着的区域控制视角转换,因此需要设计两个类JoySlicePlayerControllerJoySliceCameraController,分开控制人物移动、跳跃和视角转换,并且每一个类需要继承Drag的一些类:IBeginDragHandlerIDragHandlerIEndDragHandler,将Drag操作事件分开写,以免在同一个屏幕上相互干扰。

2、摇杆内芯的拖动方向作为人物移动的方向,其方向向量可以通过前后不同触点的位置来计算,因此可以将控制人物移动的JoySlicePlayerController脚本挂载在摇杆内芯“JoySlice”上.

3、相机和人物的旋转可以通过获取在“MovePanel”上的前后触点位置,计算方向向量实现,因此可以将控制视角转换的JoySliceCameraController脚本挂载在“MovePanel”面板对象上。

4、由于摇杆处在“MovePanel”上,因此控制相机时需要去除“JoySliceOut”和“JoySlice”区域带来的影响。

摇杆控制人物移动

人物移动的脚本我们在上一章就已经介绍过了原理和代码实现,在移动端我们只需要对其进行一次改造就行。对人物移动参数有什么疑问的可以看下上一章的介绍,这里就不重复介绍了( 传送门: Unity实现人物移动、旋转、跳跃解决方案(电脑端))。将JoySliceCameraController.cs挂在“JoySlice”组件上。

JoySliceCameraController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class JoySlicePlayerController : MonoBehaviour, IBeginDragHandler,IDragHandler,IEndDragHandler
{

    [Header("移动速度")]
    public float speed = 2f;
    [Header("检测范围")]
    public float checkRadius = 0.5f;
    [Header("检测层级")]
    public LayerMask checkLayout;
    [Header("跳跃高度")]
    public float jumpHeight = 5f;
    [Header("重力")]
    public float gravity = 9.8f;

    //玩家预设体
    private Transform player;
    private CharacterController characterController;
    //碰撞检测体
    private Transform checkGround;
    //是否接触地面
    private bool isGround;
    //下降的速度
    private Vector3 velocity;

    //首次触碰对象名称
    private string firstTouchName;
    //画布
    private Transform playerCanvas;
    //摇杆外框
    private Transform joySliceOut;
    //摇杆内芯
    private Transform joySlice;
    //跳跃按键
    private Button jumpBtn;
    //内芯初始位置
    private Vector3 originalPosition;
    //内芯移动最大半径
    private float moveMaxRadius = 0f;
    //内芯摇杆的移动方向
    private Vector3 joySliceMoveDir;

    private void Awake()
    {
        //获取摇杆内芯
        joySlice = transform;
        //获取摇杆外框
        joySliceOut = transform.parent;
        //获取画布
        playerCanvas = joySliceOut.parent.parent;
        //获取跳跃按钮
        jumpBtn = playerCanvas.Find("MovePanel/Jump").GetComponent<Button>();

        //获取人物预设体
        player = playerCanvas.parent;
        //获取人物预设体第一人称组件
        characterController = player.GetComponent<CharacterController>();
        //获取检测点
        checkGround = player.Find("CheckGround");
    }

    // Start is called before the first frame update
    void Start()
    {
        //获取内芯最大移动半径,确保摇杆内芯的中心点最大只能在摇杆外框的边缘移动
        moveMaxRadius = joySliceOut.GetComponent<RectTransform>().rect.width +joySlice.GetComponent<RectTransform>().rect.width;
        //记录摇杆内芯的初始位置,为了取消移动可以回到初始位置准备
        originalPosition = joySlice.position;
        //跳跃按钮绑定跳跃方法
        jumpBtn.onClick.AddListener(Jump);
    }

    // Update is called once per frame
    void Update()
    {
        isGround = Physics.CheckSphere(checkGround.position, checkRadius, checkLayout);
        if (isGround && velocity.y < 0)
        {
            velocity.y = -2f;
        }

        //只有初始触点是JoySlice才可以移动,确保摇杆内芯移动才可以控制人物移动
        if (firstTouchName == "JoySlice")
        {
            //这里只需要内芯移动方向的单位向量,不要距离只要方向,这样可保证人物移动速度稳定
            Vector3 playerMoveDir = player.right * joySliceMoveDir.normalized.x+ player.forward * joySliceMoveDir.normalized.y;
            characterController.Move(playerMoveDir * speed * Time.deltaTime);
        }

        velocity.y -= gravity * Time.deltaTime;
        characterController.Move(velocity * Time.deltaTime);
    }

    //手指刚触碰监听函数
    public void OnBeginDrag(PointerEventData eventData)
    {
        //获取初次点击对象的名称
        firstTouchName = eventData.pointerEnter.name;
    }

    //手指在屏幕移动监听函数
    public void OnDrag(PointerEventData eventData)
    {
        //只有手指在JoySlice上才可以拖动
        if (firstTouchName == "JoySlice")
        {
            //获取当前触点位置,由于eventData.position是Vector2,所以需要转换
            Vector3 touchPosition = new Vector3(eventData.position.x, eventData.position.y, 0);
            //计算出当前位置与初始位置的差向量
            joySliceMoveDir = touchPosition - originalPosition;
            if (joySliceMoveDir.magnitude < moveMaxRadius)
            {
                //如果触点在摇杆框内移动,则直接将触点的位置赋给joySlice,带动内芯移动
                joySlice.position = touchPosition;
            }
            else
            {
                //如果触点在摇杆框外移动,则将joySlice内芯的位置限制住,内芯的中心最大只能在外框边缘移动
                joySlice.position = (touchPosition - originalPosition).normalized * moveMaxRadius + originalPosition;
            }
        }

    }

    //手指结束拖动监听函数
    public void OnEndDrag(PointerEventData eventData)
    {
        //摇杆内芯位置复原
        joySlice.position = originalPosition;
        //摇杆内芯移动方向向量初始化
        joySliceMoveDir = Vector3.zero;
        //首次触点名称初始化
        firstTouchName = "";
    }

    //跳跃
    private void Jump()
    {
        if (isGround)
        {
            velocity.y = Mathf.Sqrt(jumpHeight * 2f * gravity);
        }
    }
}

问题:触点在摇杆框“JoySliceOut”外,摇杆内芯的位置该怎么计算?

这里先上一张图!

黑色箭头代表内芯到触点的方向向量。

红色箭头是内芯到限制内芯的方向向量。

它们两个方向相同,长度不同,可通过求单位向量乘以限制范围长度来获取限制内芯的位置(虚线圈圈)。让我们来解个方程:

x-originalPosition=(touchPosition-originalPosition).normalized*moveMaxRadius
x=(touchPosition-originalPosition).normalized*moveMaxRadius+originalPosition

限制内芯的位置就是这么算出来的。

image.png

空白区域控制视角转换

有些开发者喜欢另外创建一个摇杆来控制视角转换,但是我觉得这样操控很不舒服,在玩cf手游和和平精英的时候都是在空白区域操控的,这样让玩家的游戏体验会更好,所以我们也来这样设计!我们将JoySliceCameraController.cs脚本挂载在“MovePanel”上,关于相机在人物预设中的摆放和相关参数解释在上一章已经说过了,有需要的小伙伴可以回到上一章看看( 传送门: Unity实现人物移动、旋转、跳跃解决方案(电脑端))。

JoySliceCameraController.cs

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

public class JoySliceCameraController : MonoBehaviour, IBeginDragHandler,IDragHandler,IEndDragHandler
{
    [Header("鼠标灵敏度")]
    public float mouseSensitivity = 0.5f;
    [Header("上下旋转最小角度")]
    public float minRotate = -70f;
    [Header("上下旋转最大角度")]
    public float maxRotate = 70f;
    [Header("视角切换速度")]
    public float speed = 3f;

    //人物头部
    private Transform head;

    //上一次触点位置
    private Vector2 prePosition;
    //触点左右移动差量
    private float touchSpaceX;
    //触点上下移动差量
    private float touchSpaceY;
    //首次触碰对象名称
    private string firstTouchName;
    //判断相机是否可以旋转
    private bool isRotate = false;

    private Transform player;


    private void Awake()
    {
        //获取任务预设体
        player = transform.parent.parent;
        //获取任务预设体头部相机
        head = player.Find("Camera");
    }

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        if (isRotate)
        {
            //触点左右差量控制相机和人物预设体左右旋转
            Quaternion quaternionX = Quaternion.AngleAxis(touchSpaceX, Vector3.up);
            Quaternion quaternionY = Quaternion.AngleAxis(0, Vector3.left);
            player.rotation = quaternionX * quaternionY;
            //触点上下差量控制相机上下旋转
            quaternionY = Quaternion.AngleAxis(touchSpaceY, Vector3.left);
            head.rotation = quaternionX * quaternionY;
        }
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        //获取触点初始位置
        prePosition = eventData.position;
        //获取初次拖动触点下的对象名
        firstTouchName = eventData.pointerEnter.name;
    }

    public void OnDrag(PointerEventData eventData)
    {
        //只有初次触点对象名为MovePanel才可以计算两触点间距差量
        if (firstTouchName== "MovePanel")
        {
            //获取拖动过程中对象名
            string touchName = eventData.pointerEnter.name;
            //当前触点位置和上一次触点位置计算手指移动方向向量
            Vector3 moveDir = eventData.position - prePosition;
            //将当前触点位置作为初始位置,为下次计算使用
            prePosition = eventData.position;

            //获取触点左右移动差量
            touchSpaceX += moveDir.x * mouseSensitivity;
            //获取触点上下移动差量
            touchSpaceY += moveDir.y * mouseSensitivity;
            //对上下移动差量作范围限制,模拟人头部视角限制
            touchSpaceY = Mathf.Clamp(touchSpaceY, minRotate, maxRotate);
            //相机只有手指初次在MovePanel上,并且移动过程中不会触碰到JoySliceOut和JoySlice才可以移动,防止于摇杆拖动事件产生冲突
            isRotate = firstTouchName == "MovePanel" && touchName != "JoySliceOut" && touchName != "JoySlice" ? true : false;
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        //阻止相机转动
        isRotate = false;
        //初次触点对象名重置
        firstTouchName = "";       
    }
}

最后我们将预设体拖入场景中,启动项目,试一试摇杆、跳跃按钮和空白区域,看操作顺不顺滑。如果有安卓手机的,可以点击 File->Build Settings->Android->Build 打包成安卓包在手机上玩😊,这里记得在Player Setting中将屏幕配置成横屏。

image.png

image.png

image.png

image.png

可能出现的问题

如果有小伙伴启动编辑器发现触屏使用不了,不要慌,那是因为初次编辑没有在场景内添加UI的EventSystem,解决方法很简单,在场景随便加入一个UI对象,再删除这个不需要的对象就可以了,这时候会发现留下一个EventSystem对象,后面就可以正常使用了。

image.png

结语

到这里移动端的教学就告一段落了,如果对这两篇人物控制的文章有什么疑问,都可以评论区提出,我只要看到都会回复你们。如果觉得文章对你有用或者喜欢这一栏目的文章都可以收藏、点赞和关注,后面我还会出很多与Unity相关的实用又有趣的文章给大家看,一起体验3D给我们带来的欢乐😝!