UnityStandardAsset工程、源碼分析_1_賽車遊戲[玩家控制]_輸入系統

前言

  好久沒寫博客了。最近在做demo,感覺在遊戲架構設計方面遇到了瓶頸。在大學這幾年裏我基本是在全程造輪子,沒有去閱讀和分析別人寫的源碼,導致我缺少橫向比較,甚至不知道自己的設計是否存在問題。這幾天終於能沉下心來讀一讀別人的代碼,查缺補漏,並且吸取一下別人的經驗。
  不過好的unity開源項目還挺難找的,要麼就是像這樣設計得過於簡陋,要麼就是像OpenRA那樣有點雜七雜八。最後還是選定了Unity的StandardAsset,簡略看了一下,還是有點東西的。
  不知道能寫到什麼程度,反正寫一點是一點吧。
  用到的素材

賽車[玩家控制]

 先來看看這遊戲怎麼玩。遊戲提供了兩種操作方式:

  • 通過硬件HardwareInput操作,也就是手柄,左搖桿向前加速,向後減速,左右轉向。
  • 通過手機傾斜轉向,虛擬按鈕前進後退。

在這裏插入圖片描述
抽絲剝繭的首要任務就是找到線頭,那我們就從最核心的車輛系統入手。
這是車輛prefab的內容:
在這裏插入圖片描述
在這裏插入圖片描述

  • Colliders是車輛總體的碰撞盒,分爲三個部分,頂部,擋風玻璃,底盤,這很好理解。
  • WheelsHub是車輛的輪胎碰撞盒,分爲四個,對應車輛的四個輪胎,均爲WheelCollider。
  • Particles是車輛的尾煙,玩一下就可以知道在車輛的行駛過程中尾部會有煙塵飄起,就是這個粒子系統。
  • Helpers內WaypointTargetObject用於AI導航,這之後再說,玩家控制用不上。CameraFocusPoint是攝像機的瞄準點。
  • Light是車的兩個前大燈,爲聚光燈模式,設置爲Active以後可以亮,否則就不亮。
  • SkyCar內是車輛的Mesh,各自獨立,用途與其名字對應。

  在根節點Car上掛載有車輛的控制邏輯,此外,在四個輪胎上分別有各自的輪胎印控制器,用於模擬輪胎印,所以我們先看最核心的車輛控制邏輯。
在這裏插入圖片描述
CarController就是車輛的核心邏輯;CarUserControl是控制接口,爲用戶輸入與CarController的中間件;CarAudioCarController調用,控制聲音的播放。

我在CarController裏找了一下,並沒有發現有Update(),那邏輯是怎麼運行的呢?答案就是CarUserControl

private void FixedUpdate()
        {
            // pass the input to the car!

            float h = CrossPlatformInputManager.GetAxis("Horizontal");  //本章重點
            float v = CrossPlatformInputManager.GetAxis("Vertical");

#if !MOBILE_INPUT
            float handbrake = CrossPlatformInputManager.GetAxis("Jump");
            //m_Car是CarController類型的變量,也就是當前車輛的核心控制腳本
            //他的Move方法定義爲public void Move(float steering, float accel, float footbrake, float handbrake)
            //作用是通過幾個變量的輸入改變車輛速度,具體實現我放在下章分析,這章只考慮如何獲取輸入
            m_Car.Move(h, v, v, handbrake);
#else
            m_Car.Move(h, v, v, 0f);
#endif
        }

可以看出Unity做了平臺適配,讓不同的輸入方式在不同的平臺上得出較爲一致的結果,而作爲潤滑劑的就是CrossPlatformInputManager類,接下來我分析一下他的原理,先放代碼。

namespace UnityStandardAssets.CrossPlatformInput
{
	public static class CrossPlatformInputManager
	{
		public enum ActiveInputMethod
		{
			Hardware,
			Touch
		}
		private static VirtualInput activeInput;

		private static VirtualInput s_TouchInput;
		private static VirtualInput s_HardwareInput;

		static CrossPlatformInputManager()
		{
			s_TouchInput = new MobileInput();
			s_HardwareInput = new StandaloneInput();
			//在這裏決定使用手機輸入還是硬件輸入
#if MOBILE_INPUT
            activeInput = s_TouchInput;
#else
			activeInput = s_HardwareInput;
#endif
		}
		//切換輸入模式
		public static void SwitchActiveInputMethod(ActiveInputMethod activeInputMethod)
		{
			switch (activeInputMethod)
			{
				case ActiveInputMethod.Hardware:
					activeInput = s_HardwareInput;
					break;

				case ActiveInputMethod.Touch:
					activeInput = s_TouchInput;
					break;
			}
		}
		//接下來的方法均爲靜態調用
		public static bool AxisExists(string name)
		{
			return activeInput.AxisExists(name);
		}

		public static bool ButtonExists(string name)
		{
			return activeInput.ButtonExists(name);
		}

		public static void RegisterVirtualAxis(VirtualAxis axis)
		{
			activeInput.RegisterVirtualAxis(axis);
		}


		public static void RegisterVirtualButton(VirtualButton button)
		{
			activeInput.RegisterVirtualButton(button);
		}


		public static void UnRegisterVirtualAxis(string name)
		{
			if (name == null)
			{
				throw new ArgumentNullException("name");
			}
			activeInput.UnRegisterVirtualAxis(name);
		}


		public static void UnRegisterVirtualButton(string name)
		{
			activeInput.UnRegisterVirtualButton(name);
		}


		// returns a reference to a named virtual axis if it exists otherwise null
		public static VirtualAxis VirtualAxisReference(string name)
		{
			return activeInput.VirtualAxisReference(name);
		}


		// returns the platform appropriate axis for the given name
		public static float GetAxis(string name)
		{
			return GetAxis(name, false);
		}


		public static float GetAxisRaw(string name)
		{
			return GetAxis(name, true);
		}


		// private function handles both types of axis (raw and not raw)
		private static float GetAxis(string name, bool raw)
		{
			return activeInput.GetAxis(name, raw);
		}


		// -- Button handling --
		public static bool GetButton(string name)
		{
			return activeInput.GetButton(name);
		}


		public static bool GetButtonDown(string name)
		{
			return activeInput.GetButtonDown(name);
		}


		public static bool GetButtonUp(string name)
		{
			return activeInput.GetButtonUp(name);
		}


		public static void SetButtonDown(string name)
		{
			activeInput.SetButtonDown(name);
		}


		public static void SetButtonUp(string name)
		{
			activeInput.SetButtonUp(name);
		}


		public static void SetAxisPositive(string name)
		{
			activeInput.SetAxisPositive(name);
		}


		public static void SetAxisNegative(string name)
		{
			activeInput.SetAxisNegative(name);
		}


		public static void SetAxisZero(string name)
		{
			activeInput.SetAxisZero(name);
		}


		public static void SetAxis(string name, float value)
		{
			activeInput.SetAxis(name, value);
		}


		public static Vector3 mousePosition
		{
			get { return activeInput.MousePosition(); }
		}


		public static void SetVirtualMousePositionX(float f)
		{
			activeInput.SetVirtualMousePositionX(f);
		}


		public static void SetVirtualMousePositionY(float f)
		{
			activeInput.SetVirtualMousePositionY(f);
		}


		public static void SetVirtualMousePositionZ(float f)
		{
			activeInput.SetVirtualMousePositionZ(f);
		}

		public class VirtualAxis;//等下分析

		public class VirtualButton;//等下分析
	}
}

可見這個類的主要職責就是決定應當使用硬件輸入還是手機輸入,並且維護這個VirtualInput類型的activeInput
他的設計近似於單例模式和裝飾器模式,但不完全符合,他靜態儲存了activeInput,並提供了一系列靜態方法給用戶,用於與VirtualInput交互。本身沒有儲存什麼輸入相關的數據,只是利用靜態方法使得VirtualInput的調用更加方便。
其中有兩個內部類VirtualAxisVirtualButton等下分析,先來看看這個類最主要的維護目標VirtualInput

namespace UnityStandardAssets.CrossPlatformInput
{
    // 這是一個抽象類,在CrossPlatformInputManager中可以看到它有兩種實現,MobileInput和StandaloneInput
    // 提供三種輸入數據,虛擬光標VirtualMousePosition,軸VirtualAxis,按鍵VirtualButton
    public abstract class VirtualInput
    {
        // 利用手機的傾斜來模擬光標,與正常的鼠標輸入一樣爲Vector3
        public Vector3 virtualMousePosition { get; private set; }

        // 緩衝區,儲存了輸入的數據,作爲輸入控制器的類通過id填入數據,需要數據的類通過id獲取數據
        // 輸入控制器的作用是將原始數據轉化成方便讀取的格式,例如TiltInpt類將手機加速度轉化爲角度,再映射到[-1,1],用於控制轉向
        // 這個是軸數據的緩衝區
        // Dictionary to store the name relating to the virtual axes
        protected Dictionary<string, CrossPlatformInputManager.VirtualAxis> m_VirtualAxes =
            new Dictionary<string, CrossPlatformInputManager.VirtualAxis>();

        // 這個是按鍵數據的緩衝區
        // list of the axis and button names that have been flagged to always use a virtual axis or button
        protected Dictionary<string, CrossPlatformInputManager.VirtualButton> m_VirtualButtons =
            new Dictionary<string, CrossPlatformInputManager.VirtualButton>();
        protected List<string> m_AlwaysUseVirtual = new List<string>();
        
        // 判斷是否有名爲name的軸輸入
        public bool AxisExists(string name)
        {
            return m_VirtualAxes.ContainsKey(name);
        }

        // 判斷是否有名爲name的按鍵輸入
        public bool ButtonExists(string name)
        {
            return m_VirtualButtons.ContainsKey(name);
        }

        // 在使用之前需要註冊,這個工作是由輸入控制器完成的
        // 相當於聲明變量,在上述緩衝區中開闢一個區域,才能以id進行寫入和讀出

        // 註冊一個軸輸入
        public void RegisterVirtualAxis(CrossPlatformInputManager.VirtualAxis axis)
        {
            // id不可重複
            // check if we already have an axis with that name and log and error if we do
            if (m_VirtualAxes.ContainsKey(axis.name))
            {
                Debug.LogError("There is already a virtual axis named " + axis.name + " registered.");
            }
            else
            {
                // 註冊
                // add any new axes
                m_VirtualAxes.Add(axis.name, axis);

                // 是否匹配InputManager,暫時沒有發現有什麼用處
                // if we dont want to match with the input manager setting then revert to always using virtual
                if (!axis.matchWithInputManager)
                {
                    m_AlwaysUseVirtual.Add(axis.name);
                }
            }
        }

        // 註冊一個按鍵輸入
        public void RegisterVirtualButton(CrossPlatformInputManager.VirtualButton button)
        {
            // check if already have a buttin with that name and log an error if we do
            if (m_VirtualButtons.ContainsKey(button.name))
            {
                Debug.LogError("There is already a virtual button named " + button.name + " registered.");
            }
            else
            {
                // add any new buttons
                m_VirtualButtons.Add(button.name, button);

                // if we dont want to match to the input manager then always use a virtual axis
                if (!button.matchWithInputManager)
                {
                    m_AlwaysUseVirtual.Add(button.name);
                }
            }
        }

        // 取消一個軸輸入
        public void UnRegisterVirtualAxis(string name)
        {
            // if we have an axis with that name then remove it from our dictionary of registered axes
            if (m_VirtualAxes.ContainsKey(name))
            {
                m_VirtualAxes.Remove(name);
            }
        }

        // 取消一個按鍵輸入
        public void UnRegisterVirtualButton(string name)
        {
            // if we have a button with this name then remove it from our dictionary of registered buttons
            if (m_VirtualButtons.ContainsKey(name))
            {
                m_VirtualButtons.Remove(name);
            }
        }

        // 根據id獲取輸入,如果不存在就返回null
        // returns a reference to a named virtual axis if it exists otherwise null
        public CrossPlatformInputManager.VirtualAxis VirtualAxisReference(string name)
        {
            return m_VirtualAxes.ContainsKey(name) ? m_VirtualAxes[name] : null;
        }

        // 設置虛擬光標的x座標
        public void SetVirtualMousePositionX(float f)
        {
            virtualMousePosition = new Vector3(f, virtualMousePosition.y, virtualMousePosition.z);
        }

        // 設置虛擬光標的y座標
        public void SetVirtualMousePositionY(float f)
        {
            virtualMousePosition = new Vector3(virtualMousePosition.x, f, virtualMousePosition.z);
        }

        // 設置虛擬光標的z座標
        public void SetVirtualMousePositionZ(float f)
        {
            virtualMousePosition = new Vector3(virtualMousePosition.x, virtualMousePosition.y, f);
        }

        // 以下方法通過子類來具體實現
        public abstract float GetAxis(string name, bool raw);
        
        public abstract bool GetButton(string name);
        public abstract bool GetButtonDown(string name);
        public abstract bool GetButtonUp(string name);

        public abstract void SetButtonDown(string name);
        public abstract void SetButtonUp(string name);
        public abstract void SetAxisPositive(string name);
        public abstract void SetAxisNegative(string name);
        public abstract void SetAxisZero(string name);
        public abstract void SetAxis(string name, float value);
        public abstract Vector3 MousePosition();
    }
}

現在分析上述的三種輸入模式中的軸VirtualAxis,按鍵VirtualButton

	// virtual axis and button classes - applies to mobile input
	// Can be mapped to touch joysticks, tilt, gyro, etc, depending on desired implementation.
	// Could also be implemented by other input devices - kinect, electronic sensors, etc
	
	// 大意就是說這個類可以將各種各樣的輸入映射成可讀取的格式
	// 這其中只有一個float類型的值,代表只能存取軸上的一個分量的數據,例如x軸的值或者y軸的值,而非搖桿向量
	public class VirtualAxis
	{
		public string name { get; private set; }	// 輸入緩衝區中的id
		private float m_Value;	// 值
		public bool matchWithInputManager { get; private set; }	// 暫時不知道有啥用
	
	
		public VirtualAxis(string name)
			: this(name, true)
		{
		}
	
	
		public VirtualAxis(string name, bool matchToInputSettings)
		{
			this.name = name;
			matchWithInputManager = matchToInputSettings;
		}
	
		// 取消註冊,調用的是CrossPlatformInputManager的靜態方法,自動尋找可用的VirtualInput
		// removes an axes from the cross platform input system
		public void Remove()
		{
			UnRegisterVirtualAxis(name);
		}
	
		// 保存值
		// a controller gameobject (eg. a virtual thumbstick) should update this class
		public void Update(float value)
		{
			m_Value = value;
		}
	
		// 讀取值
		public float GetValue
		{
			get { return m_Value; }
		}
	
		// 不知道跟上一個方法有什麼區別
		public float GetValueRaw
		{
			get { return m_Value; }
		}
	}
	
	// a controller gameobject (eg. a virtual GUI button) should call the
	// 'pressed' function of this class. Other objects can then read the
	// Get/Down/Up state of this button.
	
	// 輸入控制器應當調用這個對象的Pressed()方法,其他對象就可以獲取這個按鍵的Get\Down\Up狀態
	public class VirtualButton
	{
		public string name { get; private set; }    // 輸入緩衝區中的id
		public bool matchWithInputManager { get; private set; } // 暫時不知道有啥用
	
		private int m_LastPressedFrame = -5;	// 最後一次按下按鈕的幀
		private int m_ReleasedFrame = -5;	// 釋放按鈕的幀
		private bool m_Pressed;	// 是否已被按下
	
	
		public VirtualButton(string name)
			: this(name, true)
		{
		}
	
	
		public VirtualButton(string name, bool matchToInputSettings)
		{
			this.name = name;
			matchWithInputManager = matchToInputSettings;
		}
	
	
		// A controller gameobject should call this function when the button is pressed down
		public void Pressed()
		{
			if (m_Pressed)
			{
				return;
			}
			m_Pressed = true;
			m_LastPressedFrame = Time.frameCount;
		}
	
	
		// A controller gameobject should call this function when the button is released
		public void Released()
		{
			m_Pressed = false;
			m_ReleasedFrame = Time.frameCount;
		}
	
	
		// the controller gameobject should call Remove when the button is destroyed or disabled
		public void Remove()
		{
			UnRegisterVirtualButton(name);
		}
	
		// 按鍵是否已按下
		// these are the states of the button which can be read via the cross platform input system
		public bool GetButton
		{
			get { return m_Pressed; }
		}
	
		// 按鍵是否正在被按下
		public bool GetButtonDown
		{
			get
			{
				return m_LastPressedFrame - Time.frameCount == -1;
			}
		}
	
		// 按鍵是否正在被擡起
		public bool GetButtonUp
		{
			get
			{
				return (m_ReleasedFrame == Time.frameCount - 1);
			}
		}
	}

這兩種數據都是由輸入控制器寫入數據,其他對象讀取,通過VirtualInput管理。

CrossPlatformInputManager中有這麼一段代碼:

	s_TouchInput = new MobileInput();
	s_HardwareInput = new StandaloneInput();
#if MOBILE_INPUT
    activeInput = s_TouchInput;
#else
	activeInput = s_HardwareInput;
#endif

s_TouchInput,s_HardwareInput,activeInput均爲VirtualInput類型或其子類,前兩個實現分別爲MobileInputStandaloneInput,上轉型爲VirtualInput,根據需要賦值給activeInput

接下來我們看下MobileInputStandaloneInpt的具體實現:

	public class MobileInput : VirtualInput
    {
        // 註冊按鈕
        private void AddButton(string name)
        {
            // we have not registered this button yet so add it, happens in the constructor
            CrossPlatformInputManager.RegisterVirtualButton(new CrossPlatformInputManager.VirtualButton(name));
        }

        // 註冊軸
        private void AddAxes(string name)
        {
            // we have not registered this button yet so add it, happens in the constructor
            CrossPlatformInputManager.RegisterVirtualAxis(new CrossPlatformInputManager.VirtualAxis(name));
        }

        // 獲取軸,緩衝區內沒有的話就註冊一個,下同
        public override float GetAxis(string name, bool raw)
        {
            if (!m_VirtualAxes.ContainsKey(name))
            {
                AddAxes(name);
            }
            return m_VirtualAxes[name].GetValue;
        }

        // 按下按鈕
        public override void SetButtonDown(string name)
        {
            if (!m_VirtualButtons.ContainsKey(name))
            {
                AddButton(name);
            }
            m_VirtualButtons[name].Pressed();
        }

        // 擡起按鈕
        public override void SetButtonUp(string name)
        {
            if (!m_VirtualButtons.ContainsKey(name))
            {
                AddButton(name);
            }
            m_VirtualButtons[name].Released();
        }

        // 將軸設爲正值
        public override void SetAxisPositive(string name)
        {
            if (!m_VirtualAxes.ContainsKey(name))
            {
                AddAxes(name);
            }
            m_VirtualAxes[name].Update(1f);
        }

        // 將軸設爲負值
        public override void SetAxisNegative(string name)
        {
            if (!m_VirtualAxes.ContainsKey(name))
            {
                AddAxes(name);
            }
            m_VirtualAxes[name].Update(-1f);
        }

        // 將軸設爲0
        public override void SetAxisZero(string name)
        {
            if (!m_VirtualAxes.ContainsKey(name))
            {
                AddAxes(name);
            }
            m_VirtualAxes[name].Update(0f);
        }

        // 設置軸的值
        public override void SetAxis(string name, float value)
        {
            if (!m_VirtualAxes.ContainsKey(name))
            {
                AddAxes(name);
            }
            m_VirtualAxes[name].Update(value);
        }

        // 讀取按鈕正在按下
        public override bool GetButtonDown(string name)
        {
            if (m_VirtualButtons.ContainsKey(name))
            {
                return m_VirtualButtons[name].GetButtonDown;
            }

            AddButton(name);
            return m_VirtualButtons[name].GetButtonDown;
        }

        // 讀取按鈕正在擡起
        public override bool GetButtonUp(string name)
        {
            if (m_VirtualButtons.ContainsKey(name))
            {
                return m_VirtualButtons[name].GetButtonUp;
            }

            AddButton(name);
            return m_VirtualButtons[name].GetButtonUp;
        }

        // 讀取按鈕已被按下
        public override bool GetButton(string name)
        {
            if (m_VirtualButtons.ContainsKey(name))
            {
                return m_VirtualButtons[name].GetButton;
            }

            AddButton(name);
            return m_VirtualButtons[name].GetButton;
        }

        // 獲取虛擬光標位置
        public override Vector3 MousePosition()
        {
            return virtualMousePosition;
        }
    }

	public class StandaloneInput : VirtualInput
    {
        // 獲取軸的值
        public override float GetAxis(string name, bool raw)
        {
            // 從Input類中直接獲取
            return raw ? Input.GetAxisRaw(name) : Input.GetAxis(name);
        }

        // 下同MobileInput,只不過來源爲Input類
        public override bool GetButton(string name)
        {
            return Input.GetButton(name);
        }


        public override bool GetButtonDown(string name)
        {
            return Input.GetButtonDown(name);
        }


        public override bool GetButtonUp(string name)
        {
            return Input.GetButtonUp(name);
        }

        // standaloneInput只能從硬件獲取數據,而不能被設置值,所以調用時會拋出異常
        public override void SetButtonDown(string name)
        {
            throw new Exception(
                " This is not possible to be called for standalone input. Please check your platform and code where this is called");
        }


        public override void SetButtonUp(string name)
        {
            throw new Exception(
                " This is not possible to be called for standalone input. Please check your platform and code where this is called");
        }


        public override void SetAxisPositive(string name)
        {
            throw new Exception(
                " This is not possible to be called for standalone input. Please check your platform and code where this is called");
        }


        public override void SetAxisNegative(string name)
        {
            throw new Exception(
                " This is not possible to be called for standalone input. Please check your platform and code where this is called");
        }


        public override void SetAxisZero(string name)
        {
            throw new Exception(
                " This is not possible to be called for standalone input. Please check your platform and code where this is called");
        }


        public override void SetAxis(string name, float value)
        {
            throw new Exception(
                " This is not possible to be called for standalone input. Please check your platform and code where this is called");
        }


        public override Vector3 MousePosition()
        {
            return Input.mousePosition;
        }
    }

從上述代碼來看StandaloneInput直接從硬件讀取數據,不需要也不能被其他諸如輸入控制器的類賦值的,也就是說它不需要父類VirtualInput所具有的緩存功能。MobileInput則與StandaloneInput不同,它需要輸入控制器來向緩衝區內寫入值,才能由其他類讀取值。

在這個演示工程中,通過手機傾斜與UI觸控的操作模式用到了這個功能。

  • TiltInput通過手機傾斜控制水平轉向的輸入控制器,id爲Horizontal,這個腳本被附加在CarTiltControls\TiltSteerInput這個空對象上
  • AxisTouchButton分別附加在CarTiltControls\AcceleratorCarTiltControls\Brake這兩個按鈕UI上,id均爲Vertical,但是加速的AxisValue爲1,減速的AxisValue爲-1

我們先來看TiltInput的代碼:

	public class TiltInput : MonoBehaviour
    {
        // options for the various orientations
        // 用於計算角度
        public enum AxisOptions
        {
            ForwardAxis,
            SidewaysAxis,
        }


        [Serializable]
        public class AxisMapping
        {
            public enum MappingType
            {
                NamedAxis,      // 直接控制軸
                MousePositionX, // 虛擬光標x軸方向
                MousePositionY, // 虛擬光標y軸方向
                MousePositionZ  // 虛擬光標z軸方向
            };


            public MappingType type;    // 映射方式
            public string axisName;     // 軸的id
        }


        public AxisMapping mapping; // 基礎設置
        public AxisOptions tiltAroundAxis = AxisOptions.ForwardAxis;    // 用於計算角度
        public float fullTiltAngle = 25;    // 手機傾斜角度的限制
        public float centreAngleOffset = 0; // 傾斜的初始角度偏移值


        private CrossPlatformInputManager.VirtualAxis m_SteerAxis;  // 對應的軸


        private void OnEnable()
        {
            // 初始化自己的數據儲存單元並且註冊,若是虛擬光標控制就無需存儲和註冊
            if (mapping.type == AxisMapping.MappingType.NamedAxis)
            {
                m_SteerAxis = new CrossPlatformInputManager.VirtualAxis(mapping.axisName);
                CrossPlatformInputManager.RegisterVirtualAxis(m_SteerAxis);
            }
        }


        private void Update()
        {
            float angle = 0;
            if (Input.acceleration != Vector3.zero)
            {
                switch (tiltAroundAxis)
                {
                    // 將手機加速矢量映射成角度
                    case AxisOptions.ForwardAxis:   // 手機左右擺動
                        angle = Mathf.Atan2(Input.acceleration.x, -Input.acceleration.y)*Mathf.Rad2Deg +
                                centreAngleOffset;  
                        break;
                    case AxisOptions.SidewaysAxis:  // 手機前後擺動
                        angle = Mathf.Atan2(Input.acceleration.z, -Input.acceleration.y)*Mathf.Rad2Deg +
                                centreAngleOffset;
                        break;
                }
            }

            float axisValue = Mathf.InverseLerp(-fullTiltAngle, fullTiltAngle, angle)*2 - 1;    // 將角度映射到-1,1之間
            switch (mapping.type)
            {
                case AxisMapping.MappingType.NamedAxis:
                    m_SteerAxis.Update(axisValue);  // 更新角度
                    break;
                case AxisMapping.MappingType.MousePositionX:    // 更新虛擬光標軸座標
                    CrossPlatformInputManager.SetVirtualMousePositionX(axisValue*Screen.width);
                    break;
                case AxisMapping.MappingType.MousePositionY:
                    CrossPlatformInputManager.SetVirtualMousePositionY(axisValue*Screen.width);
                    break;
                case AxisMapping.MappingType.MousePositionZ:
                    CrossPlatformInputManager.SetVirtualMousePositionZ(axisValue*Screen.width);
                    break;
            }
        }


        private void OnDisable()
        {
            m_SteerAxis.Remove();
        }
    }

以上代碼說明了TiltInput有兩種映射模式,一種是將手機擺動的數據映射成角度,再將角度映射到[-1,1]上,需要使用VirtualInput的緩存功能。另一種就是映射成虛擬光標的軸座標,無需緩存。

再來看AxisTouchButton

	public class AxisTouchButton : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
	{
		// 說是被設計成成對使用的按鈕,一個固定值-1,一個固定值1,就像前進按鈕和後退按鈕,感覺沒啥必要
		// designed to work in a pair with another axis touch button
		// (typically with one having -1 and one having 1 axisValues)
		public string axisName = "Horizontal"; // The name of the axis // 軸的id
		public float axisValue = 1; // The axis that the value has // 被按下後輸出的固定值
		public float responseSpeed = 3; // The speed at which the axis touch button responds // 朝着固定值移動的速度
		public float returnToCentreSpeed = 3; // The speed at which the button will return to its centre // 說是返回0的速度,但沒有被調用

		AxisTouchButton m_PairedWith; // Which button this one is paired with // 一對按鈕中的另一個
		CrossPlatformInputManager.VirtualAxis m_Axis; // A reference to the virtual axis as it is in the cross platform input // 儲存軸信息的類

		void OnEnable()
		{
			// 未被註冊就創建存儲類並註冊
			if (!CrossPlatformInputManager.AxisExists(axisName))
			{
				// if the axis doesnt exist create a new one in cross platform input
				m_Axis = new CrossPlatformInputManager.VirtualAxis(axisName);
				CrossPlatformInputManager.RegisterVirtualAxis(m_Axis);
			}
			else
			{
                // 已被註冊就直接獲取
				m_Axis = CrossPlatformInputManager.VirtualAxisReference(axisName);
			}
			FindPairedButton(); // 找另外一個按鈕
		}

		void FindPairedButton()
		{
			// 尋找軸id相同的另一個按鈕
			// find the other button witch which this button should be paired
			// (it should have the same axisName)
			var otherAxisButtons = FindObjectsOfType(typeof(AxisTouchButton)) as AxisTouchButton[];

			// 獲取所有AxisTouchButton對象,遍歷查找軸id相同的按鈕
			if (otherAxisButtons != null)
			{
				for (int i = 0; i < otherAxisButtons.Length; i++)
				{
					if (otherAxisButtons[i].axisName == axisName && otherAxisButtons[i] != this)
					{
						m_PairedWith = otherAxisButtons[i];
					}
				}
			}
		}

		void OnDisable()
		{
			// The object is disabled so remove it from the cross platform input system
			m_Axis.Remove();
		}

		// 使當前值朝着固定值移動
		public void OnPointerDown(PointerEventData data)
		{
			if (m_PairedWith == null)
			{
				FindPairedButton();
			}
			// update the axis and record that the button has been pressed this frame
			m_Axis.Update(Mathf.MoveTowards(m_Axis.GetValue, axisValue, responseSpeed * Time.deltaTime));
		}

		// 使當前值朝着0移動
		public void OnPointerUp(PointerEventData data)
		{
			m_Axis.Update(Mathf.MoveTowards(m_Axis.GetValue, 0, responseSpeed * Time.deltaTime));
		}
	}

這個類通過MoveTowards方法來使加速度平滑變化,而非即刻達到峯值。不過設定成結對使用的模式我不是很理解優勢在哪裏。

總結

先來總結一下重要的部分:

  • 用戶對象通過CrossPlatformInputManager的靜態方法來獲取輸入值。
  • CrossPlatformInputManager提供硬件輸入和虛擬輸入之間的切換工作,並且通過靜態屬性和靜態方法使得調用更加方便。
  • VirtualInput作爲抽象基類,提供了數據緩衝的功能,並定義了一系列方法用於讓功能不同的子類自行重寫,也讓CrossPlatformInputManager方便的調用。
  • CrossPlatformInputManager.VirtualAxis類作爲輸入數據緩衝區的存儲單元,需要註冊才能被用戶對象讀取,以及被輸入控制器寫入。
  • 輸入控制器TiltInputAxisTouchButton負責讀取用戶輸入並按某種映射模式將數據轉化爲方便使用的格式,存入緩衝區或是直接被用戶對象使用。

總體來說,這個輸入系統很好地完成了用戶輸入與核心邏輯之間的潤滑功能,但彎彎繞的註冊功能和意義不明的按鈕結對使用功能讓系統的耦合度增大了不少。

感覺今天寫了好多東西啊,下一章分析車輛的核心邏輯吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章