《土豆荣耀》重构笔记(二十二)实现虚拟摇杆和按钮


前言

  到目前为止,我们已经实现了游戏的所有功能。但目前,我们仍然使用键盘和鼠标来操作角色的移动和攻击。为了能让游戏在手机上正常运行,我们需要实现虚拟摇杆按钮,来替换键盘和鼠标输入。此外,为了减少代码的修改,且同时兼容PC端和手机端的使用,我们希望虚拟摇杆按钮的输入值的获取方式和Unity提供的Input类类似。

  注意到Unity提供的Standard Asstes下面有一个名为CrossPlatformInput的插件,这个插件的作用是进行跨平台输入适配,因此我们可以参考CrossPlatformInput插件来实现我们的虚拟摇杆和按钮。


框架设计

  Unity提供的Input类为静态类,因此我们也需要提供一个用于管理虚拟摇杆按钮输入的静态类,我们不妨将这个静态类命名为InputManager。此外,因为虚拟摇杆按钮需要根据玩家对UI进行的操作来获得输入,所以我们需要先实现只用于处理输入的VirtualAxisVirtualButton,然后在VirtualAxisVirtualButton的基础上,加上UI操作来实现虚拟摇杆按钮

框架示意图

  此外,为了方便脚本的寻找,我们在Assets\Scripts文件夹下新建一个名为Input的文件夹,用于存放所有和实现虚拟摇杆和按钮有关的脚本。


实现VirtualAxis和VirtualButton

  在清楚了框架的设计之后,我们先来实现用于处理输入的VirtualAxisVirtualButton类。在实现VirtualAxisVirtualButton类之前,我们需要了解一下VirtualAxisVirtualButton类需要管理哪些值。

  通过前面对Unity提供的Input类的使用,我们知道,当我们使用Axis时,我们可以获得一个在(-1, 1)内连续变化的float类型值。而当我们使用Button时,我们可以获取三个分别代表按钮刚刚被按下按钮被持续按下按钮刚刚被松开bool 类型值。

  也就是说,VirtualAxis需要管理一个float类型的变量,而VirtualButton需要管理三个bool类型的变量。

  了解了VirtualAxisVirtualButton类需要管理的值之后,我们先来实现较为简单的VirtualAxis类。首先,我们在Assets\Scripts\Input文件夹下新建一个名为VirtualAxis的C#脚本,然后编辑VirtualAxis.cs如下:

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

public class VirtualAxis {
    public string Name { get; private set; }
    private float m_Value;

    // 构造函数
    public VirtualAxis(string name) {
        this.Name = name;
    }

    // 更新当前的值
    public void Update(float value) {
        m_Value = value;
    }

    // 返回当前的值
    public float GetValue() {
        return m_Value; 
    }

    // 返回初始值
    public float GetValueRaw() {
        return m_Value;
    }
}

  接着,我们来实现较为复杂的VirtualButton类,我们在Assets\Scripts\Input文件夹下新建一个名为VirtualButton的C#脚本,然后编辑VirtualButton.cs如下:

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

public class VirtualButton {
    // 按钮的名称
    public string Name { get; private set; }

    // 按下按钮的帧数
    private int m_LastPressedFrame = -5;
    // 释放按钮的帧数
    private int m_ReleasedFrame = -5;
    // 按钮是否处于被按压的状态
    private bool m_Pressed;

    // 构造函数
    public VirtualButton(string name) {
        this.Name = name;
    }

    // 按压按钮
    public void Pressed() {
        if (m_Pressed) {
            return;
        }

        m_Pressed = true;
        // 记录第一次按压按钮时的帧数
        m_LastPressedFrame = Time.frameCount;
    }

    // 松开按钮
    public void Released() {
        m_Pressed = false;
        // 记录松开按钮时的帧数
        m_ReleasedFrame = Time.frameCount;
    }

    // 获取当前是否按压按钮
    public bool GetButton() {
        return m_Pressed;
    }

    // 获取当前是否刚刚按下按钮
    public bool GetButtonDown() {
        return (m_LastPressedFrame == Time.frameCount - 1);
    }

    // 获取当前是否刚刚松开按钮
    public bool GetButtonUp() {
        return (m_ReleasedFrame == Time.frameCount - 1);
    }
}

代码说明:

  1. 这里我们使用类型为boolGetButtonDownGetButtonGetBottonUp函数来获取按钮当前是否处于按钮刚刚被按下按钮被持续按下按钮刚刚被松开状态
  2. 我们知道,按钮刚刚被按下按钮刚刚被松开状态是一个短暂的状态,当按钮被按下时,按钮刚刚被按下状态只会持续一帧的时间,按钮刚刚被松开状态也一样。因此,我们使用了m_LastPressedFramem_ReleasedFrame来保存按钮刚刚被按下按钮刚刚被松开的帧数,并通过分别比较m_LastPressedFramem_ReleasedFrameTime.frameCount的帧数差来判断按钮当前是否为按钮刚刚被按下或者按钮刚刚被松开状态。

实现用于管理的InputManager

  实现完VirtualAxisVirtualButton类之后,我们还需要实现用于管理VirtualAxisVirtualButton类的静态类InputManager。我们在Assets\Scripts\Input文件夹下新建一个名为InputManager的C#脚本,然后编辑InputManager.cs如下:

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

public static class InputManager {
    private static Dictionary<string, VirtualAxis> m_VirtualAxes;
    private static Dictionary<string, VirtualButton> m_VirtualButtons;


    // 私有构造器    
    static InputManager() &#123;
        m_VirtualAxes = new Dictionary<string, VirtualAxis>();
        m_VirtualButtons = new Dictionary<string, VirtualButton>();
    &#125;

#region 用于管理的API
    // 判断轴是否存在
    public static bool AxisExists(string name) &#123;
        return m_VirtualAxes.ContainsKey(name);
    &#125;

    // 判断按钮是否注册
    public static bool ButtonExists(string name) &#123;
        return m_VirtualButtons.ContainsKey(name);
    &#125;

    // 注册Axis
    public static void RegisterVirtualAxis(VirtualAxis axis) &#123;
        // 如果已经存在同名Axis,那么提示error
        if (m_VirtualAxes.ContainsKey(axis.Name)) &#123;
            Debug.LogError("There is already a virtual axis named " + axis.Name + " registered.");
        &#125; else &#123;
            // 添加新轴
            m_VirtualAxes.Add(axis.Name, axis);
        &#125;
    &#125;

    // 注册Button
    public static void RegisterVirtualButton(VirtualButton button) &#123;
        // 如果已经存在同名Button,那么提示error
        if (m_VirtualButtons.ContainsKey(button.Name)) &#123;
            Debug.LogError("There is already a virtual button named " + button.Name + " registered.");
        &#125; else &#123;
            // 添加新按钮
            m_VirtualButtons.Add(button.Name, button);
        &#125;
    &#125;

    // 注销Axis
    public static void UnRegisterVirtualAxis(VirtualAxis axis) &#123;        
        // 删除指定的Axis
        if (m_VirtualAxes.ContainsKey(axis.Name)) &#123;
            m_VirtualAxes.Remove(axis.Name);
        &#125;
    &#125;

    // 注销Button
    public static void UnRegisterVirtualButton(VirtualButton button) &#123;
        // 删除指定的Button
        if (m_VirtualButtons.ContainsKey(button.Name)) &#123;
            m_VirtualButtons.Remove(button.Name);
        &#125;
    &#125;

    // 按下按钮
    public static void SetButtonDown(VirtualButton button) &#123;
        if(InputManager.ButtonExists(button.Name)) &#123;
            button.Pressed();
        &#125; else &#123;
                Debug.LogError("There is not a virtual button named " + button.Name + " registered.");
        &#125;
    &#125;

    // 松开按钮
    public static void SetButtonUp(VirtualButton button) &#123;
        if(InputManager.ButtonExists(button.Name)) &#123;
            button.Released();
        &#125; else &#123;
                Debug.LogError("There is not a virtual button named " + button.Name + " registered.");
        &#125;
    &#125;
#endregion

#region 用于获取输入的API
    public static float GetAxis(string name) &#123;
        if(m_VirtualAxes.ContainsKey(name)) &#123;
            return m_VirtualAxes[name].GetValue();
        &#125; else &#123;
            Debug.LogError("There is not axis named " + name + " registered.");
            return 0f;
        &#125;
    &#125;


    public static float GetAxisRaw(string name) &#123;
        if(m_VirtualAxes.ContainsKey(name)) &#123;
            return m_VirtualAxes[name].GetValueRaw();
        &#125; else &#123;
            Debug.LogError("There is not axis named " + name + " registered.");
            return 0f;
        &#125;
    &#125;


    public static bool GetButton(string name) &#123;
        if(m_VirtualButtons.ContainsKey(name)) &#123;
            return m_VirtualButtons[name].GetButton();
        &#125; else &#123;
            Debug.LogError("There is not button named " + name + " registered.");
            return false;
        &#125;
    &#125;


    public static bool GetButtonDown(string name) &#123;
        if(m_VirtualButtons.ContainsKey(name)) &#123;
            return m_VirtualButtons[name].GetButtonDown();
        &#125; else &#123;
            Debug.LogError("There is not button named " + name + " registered.");
            return false;
        &#125;
    &#125;


    public static bool GetButtonUp(string name) &#123;
        if(m_VirtualButtons.ContainsKey(name)) &#123;
            return m_VirtualButtons[name].GetButtonUp();
        &#125; else &#123;
            Debug.LogError("There is not button named " + name + " registered.");
            return false;
        &#125;
    &#125;
#endregion
&#125;

代码说明:

  1. 我们知道,我们通常会使用多个AxisButton,且我们通过Name来获取AxisButton的输入值,因此我们需要使用Dictionary来管理VirtualAxisVirtualButton,并将它们的Name设置为Key
  2. InputManager是一个静态类,因此我们需要提供一个不带访问符静态构造器,且所有函数和变量都应为Static,具体详情可以查阅C# 静态类
  3. InputManager提供了两类API,一类用于管理VirtualAxisVirtualButton的API,另外一类则是用于在Game Play中获取输入的API

实现虚拟摇杆

  实现了静态类InputManager之后,我们开始实现控制虚拟摇杆的JoyStickHandler。我们在Assets\Scripts\Input文件夹下新建一个名为JoyStickHandler的C#脚本,然后编辑JoyStickHandler.cs如下:

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

public class JoyStickHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IDragHandler &#123;
    public enum AxisOption &#123;
        // 使用哪个轴
        Both,            // 使用两个轴
        Horizontal,     // 使用水平轴
        Vertical         // 使用数值轴
    &#125;

    [Tooltip("虚拟摇杆的最大活动范围")]
    public float Range = 100;
    [Tooltip("是否根据屏幕的尺寸对虚拟摇杆的最大获得范围进行缩放")]
    public bool ScaleRange = true;
    [Tooltip("使用哪个轴")]
    public AxisOption AxisToUse = AxisOption.Both;
    [Tooltip("水平轴的名称")]
    public string HorizontalAxisName = "Horizontal";
    [Tooltip("数值轴的名称")]
    public string VerticalAxisName = "Vertical";

    Vector3 m_StartPos;
    bool m_UseHorizontalAxis;
    bool m_UseVerticalAxis;
    VirtualAxis m_HorizontalVirtualAxis;
    VirtualAxis m_VerticalVirtualAxis;

    private void Awake() &#123;
        // 确保虚拟摇杆运动的位置一样
        if(ScaleRange)&#123;
            CanvasScaler scaler = transform.root.GetComponent<CanvasScaler>();

            float scaleX = Screen.width / scaler.referenceResolution.x;
            float scaleY = Screen.height / scaler.referenceResolution.y;

            if(scaleX > scaleY) &#123;
                Range *= scaleY;
            &#125; else &#123;
                Range *= scaleX;
            &#125;
        &#125;

        m_UseHorizontalAxis = (AxisToUse == AxisOption.Both || AxisToUse == AxisOption.Horizontal);
        m_UseVerticalAxis = (AxisToUse == AxisOption.Both || AxisToUse == AxisOption.Vertical);

        // 启用时对轴进行注册
        if (m_UseHorizontalAxis) &#123;
            m_HorizontalVirtualAxis = new VirtualAxis(HorizontalAxisName);
        &#125;

        if (m_UseVerticalAxis) &#123;
            m_VerticalVirtualAxis = new VirtualAxis(VerticalAxisName);
        &#125;
    &#125;

    private void OnEnable() &#123;
        // 启用时对轴进行注册
        if (m_UseHorizontalAxis) &#123;
            InputManager.RegisterVirtualAxis(m_HorizontalVirtualAxis);
        &#125;

        if (m_UseVerticalAxis) &#123;
            InputManager.RegisterVirtualAxis(m_VerticalVirtualAxis);
        &#125;
    &#125;

    private void Start() &#123;
        m_StartPos = transform.position;
    &#125;

    private void OnDisable() &#123;
        // 禁用时取消轴注册
        if (m_UseHorizontalAxis) &#123;
            InputManager.UnRegisterVirtualAxis(m_HorizontalVirtualAxis);
        &#125;

        if (m_UseVerticalAxis) &#123;
            InputManager.UnRegisterVirtualAxis(m_VerticalVirtualAxis);
        &#125;
    &#125;

    // 更新摇杆的位置和轴的值
    private void UpdateVirtualAxes(Vector3 delta) &#123;
        transform.position = new Vector3(
            m_StartPos.x + delta.x, 
            m_StartPos.y + delta.y, 
            m_StartPos.z + delta.z
        );

        // 这里需要除以Range而不是归一化
        delta /= Range;

        if (m_UseHorizontalAxis) &#123;
            m_HorizontalVirtualAxis.Update(delta.x);
        &#125;

        if (m_UseVerticalAxis) &#123;
            m_VerticalVirtualAxis.Update(delta.y);
        &#125;
    &#125;

#region  接口函数
    // 拖拽虚拟遥杆
    public void OnDrag(PointerEventData data) &#123;
        Vector3 newPos = Vector3.zero;

        // 更新水平轴的位置和值
        if (m_UseHorizontalAxis) &#123;
            float delta = data.position.x - m_StartPos.x;
            newPos.x = delta;
        &#125;


        if (m_UseVerticalAxis) &#123;
            float delta = data.position.y - m_StartPos.y;
            newPos.y = delta;
        &#125;

        // 确保运动范围为半径为Range的圆
        if(newPos.magnitude > Range) &#123;
            newPos = newPos.normalized * Range;
        &#125;

        UpdateVirtualAxes(newPos);
    &#125;

    // 松开虚拟摇杆
    public void OnPointerUp(PointerEventData data) &#123;
        UpdateVirtualAxes(Vector3.zero);
    &#125;

    // 点击虚拟摇杆
    public void OnPointerDown(PointerEventData data) &#123;

    &#125;
#endregion
&#125;

代码说明:

  1. JoyStickHandler.cs脚本需要附加到Game Object上,因此JoyStickHandler类需要继承MonoBehaviour
  2. 根据前面的框架设计,JoyStickHandler类除了要使用VirtualAxis,还需要根据用户对UI的操作来获取输入,因此我们需要让JoyStickHandler实现位于UnityEngine.EventSystems命名空间下的IPointerDownHandlerIPointerUpHandlerIDragHandler接口,从而获取UI的按下松开拖拽事件
  3. 当我们勾选ScaleRange时,我们需要根据屏幕尺寸对Range进行缩放,从而保证在不同尺寸的屏幕上,摇杆移动的最大距离一致
  4. 为了减少InputManager的管理量,当虚拟摇杆被禁用时,我们需要注销对应的Axis,当虚拟摇杆被启用时,我们在重新注册对应的Axis

实现按钮

  实现了虚拟摇杆之后,我们接着实现控制按钮的ButtonHandler。我们在Assets\Scripts\Input文件夹下新建一个名为ButtonHandler的C#脚本,然后编辑ButtonHandler.cs如下:

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

[RequireComponent(typeof(Image))]
public class ButtonHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler &#123;
    [Tooltip("激活按钮使用的名字")]
    public string Name;
    [Tooltip("按钮松开时显示的图片")]
    public Sprite NormalImage;
    [Tooltip("按钮被按下时显示的图片")]
    public Sprite ActiveImage;
    [Tooltip("按钮被禁用时显示的图片")]
    public Sprite DisableImage;

    private Image m_CurrentImage;
    private VirtualButton m_Button;

    private void Awake() &#123;
        m_CurrentImage = GetComponent<Image>();
        // 创建按钮
        m_Button = new VirtualButton(Name);
    &#125;

    private void OnEnable() &#123;
        InputManager.RegisterVirtualButton(m_Button);
        if(NormalImage != null) &#123;
            m_CurrentImage.sprite = NormalImage;
        &#125;
    &#125;

    private void OnDisable() &#123;
        InputManager.UnRegisterVirtualButton(m_Button);
        if(DisableImage != null) &#123;
            m_CurrentImage.sprite = DisableImage;
        &#125;
    &#125;
#region  接口函数
    // 松开虚拟摇杆
    public void OnPointerUp(PointerEventData data) &#123;
        InputManager.SetButtonUp(m_Button);

        if(NormalImage != null) &#123;
            m_CurrentImage.sprite = NormalImage;
        &#125;
    &#125;

    // 点击虚拟摇杆
    public void OnPointerDown(PointerEventData data) &#123;
        InputManager.SetButtonDown(m_Button);

        if(ActiveImage != null) &#123;
            m_CurrentImage.sprite = ActiveImage;
        &#125;
    &#125;
#endregion
&#125;

代码说明:

  1. ButtonHandler.cs脚本需要附加到Game Object上,因此ButtonHandler类需要继承MonoBehaviour
  2. 因为按钮不可以被拖拽,因此我们只需要实现位于UnityEngine.EventSystems命名空间下的IPointerDownHandlerIPointerUpHandler接口
  3. 为了减少InputManager的管理量,当按钮被禁用时,我们需要注销对应的Button,当按钮被启用时,我们在重新注册对应的Button
  4. 在这里,我们默认Button都需要使用Image可以根据自己的具体需求来拓展

为游戏场景添加虚拟摇杆和按钮

  按钮和虚拟摇杆都实现了之后,我们开始为游戏场景添加虚拟摇杆和按钮。为了和游戏场景的UI区分开来,我们需要另外创建一个用于绘制虚拟摇杆和按钮的Canvas。

创建绘制虚拟摇杆和按钮的Canvas的步骤:

  1. 在游戏场景中新建一个Canvas,并将其重命名为InputCanvas
  2. 接着修改InputCanvasCanvas Scaler组件

    Canvas Scaler组件的属性

    • UI Scale Mode: Scale With Screen Size
    • Reference Resolution: X(1920), Y(1080)
    • Screen Match Mode: Match Width or Height
    • Match: 0.5
  3. 最后,我们需要确保原先的UICanvas绘制在InputCanvas之上,因此我们需要修改UICanvasCanvas组件的Sort Order属性为1

  创建好绘制虚拟摇杆和按钮的Canvas之后,我们先来添加虚拟摇杆。

添加虚拟摇杆的步骤如下:

  1. InputCanvas下创建一个Image,然后将其重命名为JoyStickBackground

    JoyStickBackground物体需要修改的组件属性:

    • Rect Transform:
      • Anchors: 点击Rect Transform组件左上角的方框,然后选择bottom-left
      • PosX: 250
      • PosY: 210
      • Width: 300
      • Height: 300
    • Image:
      • Source Image: Assets\Sprites\UI文件夹下的RadialJoy_Area图片
      • Color: (255, 255, 255, 255)
  2. JoyStickBackground下创建一个Image,然后将其重命名为JoyStick,并在JoyStick物体上添加JoyStickHandler.cs脚本

    JoyStick物体需要修改的组件属性:

    • Rect Transform:
      • PosX: 0
      • PosY: 0
      • Width: 150
      • Height: 150
    • Image:
      • Source Image: Assets\Sprites\UI文件夹下的RadialJoy_Touch图片
      • Color: (255, 255, 255, 255)
    • JoyStickHandler (Script)
      • Range: 100
      • Scale Range: true
      • Axis To Use: Both
      • Horizontal Axis Name: Horizontal
      • Vertical Axis Name: Vertical

  添加完虚拟摇杆之后,我们继续添加按钮

添加按钮的步骤:

  1. InputCanvas下创建一个Image,然后将其重命名为Fire1Button,并在Fire1Button物体上添加ButtonHandler.cs脚本

    Fire1Button物体需要修改的组件属性:

    • Rect Transform:
      • Anchors: 点击Rect Transform组件左上角的方框,然后选择bottom-right
      • PosX: -160
      • PosY: 140
      • Width: 200
      • Height: 200
    • Image:
      • Source Image: Assets\Sprites\UI文件夹下的Button_normal图片
      • Color: (255, 255, 255, 255)
    • ButtonHandler (Script)
      • Name: Fire1
      • Normal Image: Assets\Sprites\UI文件夹下的Button_normal图片
      • Active Image: Assets\Sprites\UI文件夹下的Button_active图片
      • Diasble Image: Assets\Sprites\UI文件夹下的Button_normal图片
  2. Fire1Button物体上创建一个Image

    Image物体需要修改的组件属性:

    • Rect Transform:
      • PosX: 0
      • PosY: 0
      • Width: 140
      • Height: 60
    • Image:
      • Source Image: Assets\Sprites\Props文件夹下的part_rocket图片
      • Color: (255, 255, 255, 255)
  3. 复制三次Fire1Button物体得到Fire1Button (1)Fire1Button (2)Fire1Button (3)物体
  4. Fire1Button (1)重命名为JumpButton

    JumpButton物体需要修改的组件属性:

    • Rect Transform:
      • PosX: -380
      • PosY: 140
      • Width: 150
      • Height: 150
    • ButtonHandler (Script)
      • Name: Jump
  5. JumpButton物体上创建一个Text,并删除JumpButton物体下的Image子物体

    Text物体需要修改的组件属性:

    • Rect Transform:
      • Anchors: 点击Rect Transform组件左上角的方框,然后选择strentch-strentch
      • Left: 0
      • Top: 0
      • Right: 0
      • Bottom: 0
    • Text:
      • Text: Jump
      • Font: Assets\Fonts下的BradBunR字体文件
      • Font Size: 58
      • Alignment: 水平居中,垂直居中
      • Color: (50, 50, 50, 255)
  6. Fire1Button (2)重命名为Fire2Button

    Fire2Button物体需要修改的组件属性:

    • Rect Transform:
      • PosX: -351.6
      • PosY: 295.6
      • Width: 150
      • Height: 150
    • ButtonHandler (Script)
      • Name: Fire2
  7. 接着,我们修改Fire2Button物体下的Image子物体

    Image物体需要修改的组件属性:

    • Rect Transform:
      • PosX: 0
      • PosY: 0
      • Width: 100
      • Height: 100
    • Image:
      • Source Image: Assets\Sprites\Props文件夹下的prop_bomb图片
  8. Fire1Button (3)重命名为Fire3Button

    Fire3Button物体需要修改的组件属性:

    • Rect Transform:
      • PosX: -160
      • PosY: 360
      • Width: 150
      • Height: 150
    • ButtonHandler (Script)
      • Name: Fire3
  9. 最后,我们修改Fire1Button (3)物体下的Image子物体

    Image物体需要修改的组件属性:

    • Rect Transform:
      • PosX: 0
      • PosY: 0
      • Width: 110
      • Height: 60
    • Image:
      • Source Image: Assets\Sprites\Character文件夹下char_hero_beanMan图集切割出来的bazooka图片

  添加完虚拟摇杆和按钮之后的效果图如下所示:

添加完虚拟摇杆和按钮之后的效果图


使用虚拟摇杆和按钮

  添加完虚拟摇杆和按钮之后,我们需要将之前使用Unity提供Input类来获取输入的脚本改为使用InputManager类来获取输入。首先,我们修改Assets\Scripts\Player文件夹下的PlayerAttack.cs:

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

// [RequireComponent(typeof(Animator))]
[RequireComponent(typeof(PlayerController))]
public class PlayerAttack : MonoBehaviour &#123;
    [Tooltip("导弹Prefab")]
    public Missile MissilePrefab;
    [Tooltip("导弹发射点")]
    public Transform ShootingPoint;
    [Tooltip("发射导弹的音效")]
    public AudioClip ShootEffect;
    [Tooltip("炸弹Prefab")]
    public Rigidbody2D BombPrefab;
    [Tooltip("使用火箭筒抛射炸弹的力")]
    public float ProjectileBombForce = 1000f;

    // private Animator m_Animator;
    private PlayerController m_PlayerCtrl;

    private void Awake() &#123;
        // 获取引用
        // m_Animator = GetComponent<Animator>();
        m_PlayerCtrl = GetComponent<PlayerController>();

        // 检查关键属性是否赋值
        if(MissilePrefab == null) &#123;
            Debug.LogError("请设置MissilePrefab");
        &#125;

        if(ShootingPoint == null) &#123;
            Debug.LogError("请设置ShootingPoint");
        &#125;

        if(BombPrefab == null) &#123;
            Debug.LogError("请设置BombPrefab");
        &#125;
    &#125;

    private void Update() &#123;
#if UNITY_STANDALONE                                //PC端使用Input来获取输入
        if (Input.GetButtonDown("Fire1")) &#123;
            // 发射导弹
            Fire();
        &#125;

        if (Input.GetButtonDown("Fire2")) &#123;
            // 放置炸弹
            LayBomb();
        &#125;

        if (Input.GetButtonDown("Fire3")) &#123;
            // 抛射炸弹
            ProjectileBomb();
        &#125;
#elif UNITY_IOS || UNITY_ANDROID                    //移动端使用InputManager来获取输入
        if (InputManager.GetButtonDown("Fire1")) &#123;
            // 发射导弹
            Fire();
        &#125;

        if (InputManager.GetButtonDown("Fire2")) &#123;
            // 放置炸弹
            LayBomb();
        &#125;

        if (InputManager.GetButtonDown("Fire3")) &#123;
            // 抛射炸弹
            ProjectileBomb();
        &#125;
#endif
    &#125;

    // 发射导弹
    private void Fire() &#123;
        // // 播放射击动画
        // m_Animator.SetTrigger("Shoot");

        // 播放射击音效
        AudioSource.PlayClipAtPoint(ShootEffect, ShootingPoint.position);

        // 创建导弹
        Missile instance = Instantiate(MissilePrefab, ShootingPoint.position, Quaternion.identity) as Missile;

        // 如果角色跟导弹的朝向不一致,就翻转导弹
        if(m_PlayerCtrl.FacingRight ^ instance.FacingRight) &#123;
            instance.Flip();
        &#125;
    &#125;

    // 放置炸弹
    private void LayBomb() &#123;
        // 判断当前是否至少有一颗炸弹可以释放
        if(GameStateManager.Instance.BombManagerInstance.ReleaseBomb(1) == false) &#123;
            return;
        &#125;

        // 放置炸弹
        Instantiate(BombPrefab, this.transform.position, Quaternion.identity);
    &#125;

    // 抛射炸弹
    private void ProjectileBomb() &#123;
        // 判断当前是否至少有一颗炸弹可以释放
        if(GameStateManager.Instance.BombManagerInstance.ReleaseBomb(1) == false) &#123;
            return;
        &#125;

        // 抛射炸弹
        Rigidbody2D body = Instantiate(BombPrefab, ShootingPoint.position, Quaternion.identity) as Rigidbody2D;
        if(m_PlayerCtrl.FacingRight) &#123;
            body.AddForce(Vector2.right * ProjectileBombForce);
        &#125; else &#123;
            body.AddForce(Vector2.left * ProjectileBombForce);
        &#125;
    &#125;
&#125;

  首先,我们修改Assets\Scripts\Player文件夹下的PlayerController.cs:

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

[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(Animator))]
public class PlayerController : MonoBehaviour &#123;
    [Tooltip("角色初始朝向是否朝向右边")]
    public bool FacingRight = true;
    [Tooltip("移动时角色加速的力大小")]
    public float MoveForce = 365f;
    [Tooltip("角色移动的最大速度")]
    public float MaxSpeed = 5f;
    [Tooltip("跳跃时向上加速的力大小")]
    public float JumpForce = 1000f;
    [Tooltip("检测角色是否落地")]
    public Transform GroundCheck;

    [Tooltip("跳跃音效")]
    public AudioClip[] JumpClips;

    [Tooltip("显示血量条的物体")]
    public Transform HealthBarDisplay;

    // 获取用户输入
    private Vector2 m_Input;
    // 记录角色当前是否处于准备跳跃状态
    private bool m_IsReadyToJump;
    // 记录角色当前是否正处于跳跃状态
    private bool m_IsJumping;
    // 记录角色当前是否处于着地状态
    private bool m_GroundedStatus;

    // 组件引用变量
    private Rigidbody2D m_Rigidbody2D;
    private Animator m_Animator;

    private void Awake() &#123;
        // 获取组件引用
        m_Rigidbody2D = GetComponent<Rigidbody2D>();
        m_Animator = GetComponent<Animator>();
    &#125;

    private void Start() &#123;
        // 监测变量是否正确赋值
        if(GroundCheck == null) &#123;
            Debug.LogError("请先设置GroundCheck");
        &#125;

        // 初始化变量
        m_Input = new Vector2();
        m_IsReadyToJump = false;
        m_IsJumping = false;
        m_GroundedStatus = false;
    &#125;

    private void Update() &#123;
        // 通过检测角色和groundCheck之间是否存在Ground层的物体来判断当前是否落地
        m_GroundedStatus = Physics2D.Linecast(
            transform.position,
            GroundCheck.position,
            LayerMask.GetMask("Obstacle")
        );

        // 设置动画状态机控制参数
        m_Animator.SetBool("Grounded", m_GroundedStatus);

#if UNITY_STANDALONE                                //PC端使用Input来获取输入
        // 着地时,如果当前不处于跳跃状态且按下了跳跃键,进入准备跳跃状态
        if(m_GroundedStatus && !m_IsJumping && Input.GetButtonDown("Jump")) &#123;
            m_IsReadyToJump = true;
        &#125;

        m_Input.x = Input.GetAxis("Horizontal");
#elif UNITY_IOS || UNITY_ANDROID                    //移动端使用InputManager来获取输入
        // 着地时,如果当前不处于跳跃状态且按下了跳跃键,进入准备跳跃状态
        if(m_GroundedStatus && !m_IsJumping && InputManager.GetButtonDown("Jump")) &#123;
            m_IsReadyToJump = true;
        &#125;

        m_Input.x = InputManager.GetAxis("Horizontal");
#endif

        // 刚刚落地,退出跳跃状态
        if(m_GroundedStatus && m_IsJumping) &#123;
            m_IsJumping = false;
        &#125;
    &#125;

    private void FixedUpdate() &#123;
        //获取水平输入
        float h = m_Input.x;

        // 设置动画状态机控制参数
        m_Animator.SetFloat("Speed", Mathf.Abs(h));

        // 若h * m_Rigidbody2D.velocity.x为正数且小于MaxSpeed,表示需要继续加速
        // 若h * m_Rigidbody2D.velocity.x为负数,则表示需要反向加速
        if(h * m_Rigidbody2D.velocity.x < MaxSpeed) &#123;
            m_Rigidbody2D.AddForce(Vector2.right * h * MoveForce);
        &#125;

        //设置物体速度的阈值
        if(Mathf.Abs(m_Rigidbody2D.velocity.x) > MaxSpeed) &#123;
            m_Rigidbody2D.velocity = new Vector2(
                Mathf.Sign(m_Rigidbody2D.velocity.x) * MaxSpeed,
                m_Rigidbody2D.velocity.y
            );
        &#125;

        //判断当前是否需要转向
        if(h > 0 && !FacingRight) &#123;
            Flip();
        &#125;else if(h < 0 && FacingRight) &#123;
            Flip();
        &#125;

        // 跳跃
        if(m_IsReadyToJump) &#123;
            Jump();
        &#125;
    &#125;

    private void Jump() &#123;
        // 进入跳跃状态
        m_IsJumping = true;

        // 设置一个竖直向上的力
        m_Rigidbody2D.AddForce(new Vector2(0f, JumpForce));

        // 设置动画状态机控制参数
        m_Animator.SetTrigger("Jump");

        // 退出准备跳跃状态,避免重复跳跃
        m_IsReadyToJump = false;

        //随机在角色当前所处的位置播放一个跳跃的音频
        if(JumpClips.Length > 0) &#123;
            int i = Random.Range(0, JumpClips.Length);
            AudioSource.PlayClipAtPoint(JumpClips[i], transform.position);
        &#125;
    &#125;

    private void Flip() &#123;
        // 修改当前朝向
        FacingRight = !FacingRight;

        // 修改scale的x分量实现转向
        this.transform.localScale = Vector3.Scale(
            new Vector3(-1, 1, 1),
            this.transform.localScale
        );

        if(HealthBarDisplay != null) &#123;
            // 在角色转向时翻转HealthBarDisplay,确保HealthBarDisplay不随角色转向而翻转
            HealthBarDisplay.localScale = Vector3.Scale(
                new Vector3(-1, 1, 1),
                HealthBarDisplay.localScale
            );
        &#125; else &#123;
            Debug.LogWarning("请设置HealthBarDisplay");
        &#125;
    &#125;
&#125;

代码说明:

  1. 因为PlayController没有使用到m_AduioScoure成员变量,因此我们将和这个变量有关的代码全部删除
  2. 之前我们在FixedUpdate中获取Horizontal Axis的输入,通常来说,获取输入这一操作应该在Update函数中执行,因此我们新增一个m_InputUpdate中获取输入,然后在FixedUpdate函数中使用m_Input

  修改完毕之后,运行游戏,发现当我们拖拽虚拟摇杆的时候,角色可以左右移动。当我们点击按钮的时候,角色会根据我们点击的按钮执行相应的操作。


后言

  至此,我们就已经完成了实现虚拟摇杆和按钮的全部工作。需要说明的是,我们InputManager并没有根据当前的平台自动来判断是否需要显示虚拟摇杆和按钮的功能,我们可以根据自己的具体需求自行进行拓展。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay20分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Standard Asstes
  2. C# 静态类
  3. IPointerDownHandler接口
  4. IPointerUpHandler接口
  5. IDragHandler接口

文章作者: RainbowCyan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 RainbowCyan !
 上一篇
《土豆荣耀》重构笔记(二十三)打包游戏 《土豆荣耀》重构笔记(二十三)打包游戏
前言  在上篇文章中,我们实现了虚拟摇杆和按钮,完成了游戏所有的开发工作,是时候对游戏进行打包,在手机上进行测试了。 修改Player Settings  在进行打包前,我们需要修改一下游戏的Pla
下一篇 
《土豆荣耀》重构笔记(二十一)制作游戏菜单场景 《土豆荣耀》重构笔记(二十一)制作游戏菜单场景
前言  到目前为止,我们游戏中只有一个游戏场景,我们还需要制作一个游戏菜单场景,让玩家可以选择开始游戏。 创建游戏菜单场景  首先,我们在顶部菜单栏选择File->New Scene,然后按快
  目录