本文主要是介绍Unity热更新方案C#Like(廿三)-实战:示范如何把Unity官方免费例子Tanks! Tutorial转成可热更新项目,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
C#Like是Unity的热更方案,使用纯C#语言写出可以热更新的代码,就像可以在所有平台使用DLL(动态链接库)文件一样.遵从KISS设计原则,让用户轻松构建或升级成Unity的热更新项目.
简介
本篇主要介绍如何一步一步地详细地把Unity官方免费例子Tanks! Tutorial转成可热更新项目.
搭建的详细步骤
我们首先选取一个最少代码的Unity出品的免费的例子"Tanks! Tutorial"作为示范,即官方Demo里的"TankFree"项目是如何制作的,下面是详细步骤:
-
创建一个空白的2D/3D项目为TankFree
-
到Unity资源商店购买相关的免费资源:
如果已购买则跳过本步骤- 免费购买Tanks! Tutorial
- 免费购买C#Like免费版
-
导入相关的免费资源:
- 先打开Package Manager界面 : 菜单'Window'->'Package Manager'
- 导入'Tanks! Tutorial' : 'Packages: My Assets'->'Tanks! Tutorial'->'Import'
- 导入'C#Like免费版' : 'Packages: My Assets'->'C#LikeFree Hot Update Framework'->'Import'
-
移除默认导出C#Like内置资源的AssetBundle: 你也可以直接删掉它
- Project面板选中"Assets\C#Like\Scenes\BuildInDemoScene"文件后, 在Inspector面板里的Asset Labels的AssetBundle设置为none
- Project面板选中"Assets\C#Like\Sample"目录后, 在Inspector面板里的Asset Labels的AssetBundle设置为none
-
修改产品名称:
- 菜单'Edit'->'Project Settings'->'Player'->'Product Name' 设置为 'Tank'
-
修改脚本:
- 删除目录'\Assets\Scripts'. 因为热更脚本里的类不能使用和没有命名空间的类名取相同的名字, 例如非热更脚本里有个不带命名空间的类叫'ClassName',热更新脚本里不能存在命名空间不同但类名相同的类'NamespaceName.ClassName', 所以建议所有的类都加上命名空间吧.
- 复制目录'\Assets\_Completed-Assets\Scripts' 到 '\Assets\_Completed-Assets\HotUpdateScripts'. 即我们将要把原来目录'\Assets\_Completed-Assets\Scripts'里的非热更脚本改成目录'\Assets\_Completed-Assets\HotUpdateScripts'里的热更新脚本
- 最终改好后的脚本文件详见GitHub里的TankFree项目,下面简述一些注意的事项:
- 把原命名空间'Complete'改为'CSharpLike'.这个纯粹看你个人爱好,没限定改成什么.
- 把基类'MonoBehaviour'的都改为'LikeBehaviour'
- 按需确定要采用不同的HotUpdateBehaviourXXXXX.详见'\Assets\C#Like\Runtime\Interaction\HotUpdateBehaviourCustom'
- 例如'Assets\_Completed-Assets\HotUpdateScripts\Camera\CameraControl.cs'里面用到FixedUpdate, 所以我们采用HotUpdateBehaviourFixedUpdate来绑定预制体
- 例如'Assets\_Completed-Assets\HotUpdateScripts\Shell\ShellExplosion.cs'里面用到OnTriggerEnter, 所以我们采用HotUpdateBehaviourTrigger来绑定预制体
- 根据你的项目,强力建议你自行仿造地编写一些非热更新定制绑定脚本, 放入'\Assets\C#Like\Runtime\Interaction\HotUpdateBehaviourCustom'目录内, 可以参考'Assets\C#Like\Runtime\Interaction\HotUpdateBehaviourCustom\HotUpdateBehaviourAll.cs'来定制
- 如果你的项目已上线且找不到合适的绑定,就使用HotUpdateBehaviourAll吧
- 根据绑定不同需求,在Awake或Start里绑定预制体里的数据 (int/bool/double/float/string/Color/Vector3/GameObject/TextAsset/Material/Texture/AudioClip/Sprite)
- 注意生命周期顺序 : Awake -> OnEnable -> Start. 如果绑定其他LikeBehaviour对象的数据,尽量放在Start函数而非Awake,因为在Awake的时候,其他对象尚未执行初始化
- C#Like免费版不支持协程,需要用Update来代替,更多免费版和完整版的区别详见:主页
- WebGL下,AudioClip不知为何会报错无法识别,最终使用'ResourceManager.LoadAudioClipAsync'来下载音频文件后绑上的. 奇怪,不知为何'Platformer Microgame'又没问题
- 然后我们逐一地把修改目录'\Assets\_Completed-Assets\HotUpdateScripts'里的所有cs脚本文件:
- 我们复制'Assets/_Completed-Assets/Scripts'整个目录为'Assets/_Completed-Assets/HotUpdateScripts'目录, 里面包含8个脚本文件,里面的8个cs文件是待修改成热更新脚本, 和原代码改动部分会标注 '// RongRong : '
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Camera/CameraControl.cs',以下是完整版和免费版对应的改后热更新代码:
//C#Like完整版 using UnityEngine; namespace CSharpLike//RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Awake/FixedUpdate', /// 我们使用 'HotUpdateBehaviourFixedUpdate' 来绑定预制体 /// 预制体里设置'Floats': "m_DampTime"/"m_ScreenEdgeBuffer"/"m_MinSize". /// </summary> public class CameraControl : LikeBehaviour //RongRong : 基类'MonoBehaviour'改为'LikeBehaviour' { public float m_DampTime = 0.2f; // Approximate time for the camera to refocus. public float m_ScreenEdgeBuffer = 4f; // Space between the top/bottom most target and the screen edge. public float m_MinSize = 6.5f; // The smallest orthographic size the camera can be. [HideInInspector] public Transform[] m_Targets; // All the targets the camera needs to encompass. private Camera m_Camera; // Used for referencing the camera. private float m_ZoomSpeed; // Reference speed for the smooth damping of the orthographic size. private Vector3 m_MoveVelocity; // Reference velocity for the smooth damping of the position. private Vector3 m_DesiredPosition; // The position the camera is moving towards. private void Awake () { m_Camera = gameObject.GetComponentInChildren<Camera> ();// RongRong : 加前缀"gameObject." // RongRong : 绑定预制体里的值. m_DampTime = GetFloat("m_DampTime", 0.2f); m_ScreenEdgeBuffer = GetFloat("m_ScreenEdgeBuffer", 4f); m_MinSize = GetFloat("m_MinSize", 6.5f); } private void FixedUpdate () { // Move the camera towards a desired position. Move (); // Change the size of the camera based. Zoom (); } private void Move () { // Find the average position of the targets. FindAveragePosition (); // Smoothly transition to that position. transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime); } private void FindAveragePosition () { Vector3 averagePos = new Vector3 (); int numTargets = 0; // Go through all the targets and add their positions together. for (int i = 0; i < m_Targets.Length; i++) { // If the target isn't active, go on to the next one. if (!m_Targets[i].gameObject.activeSelf) continue; // Add to the average and increment the number of targets in the average. averagePos += m_Targets[i].position; numTargets++; } // If there are targets divide the sum of the positions by the number of them to find the average. if (numTargets > 0) averagePos /= numTargets; // Keep the same y value. averagePos.y = transform.position.y; // The desired position is the average position; m_DesiredPosition = averagePos; } private void Zoom () { // Find the required size based on the desired position and smoothly transition to that size. float requiredSize = FindRequiredSize(); m_Camera.orthographicSize = Mathf.SmoothDamp (m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime); // RongRong : C#LikeFree not support 'ref' } private float FindRequiredSize () { // Find the position the camera rig is moving towards in its local space. Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition); // Start the camera's size calculation at zero. float size = 0f; // Go through all the targets... for (int i = 0; i < m_Targets.Length; i++) { // ... and if they aren't active continue on to the next target. if (!m_Targets[i].gameObject.activeSelf) continue; // Otherwise, find the position of the target in the camera's local space. Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position); // Find the position of the target from the desired position of the camera's local space. Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos; // Choose the largest out of the current size and the distance of the tank 'up' or 'down' from the camera. size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.y)); // Choose the largest out of the current size and the calculated size based on the tank being to the left or right of the camera. size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.x) / m_Camera.aspect); } // Add the edge buffer to the size. size += m_ScreenEdgeBuffer; // Make sure the camera's size isn't below the minimum. size = Mathf.Max (size, m_MinSize); return size; } public void SetStartPositionAndSize () { // Find the desired position. FindAveragePosition (); // Set the camera's position to the desired position without damping. transform.position = m_DesiredPosition; // Find and set the required size of the camera. m_Camera.orthographicSize = FindRequiredSize (); } } } //C#Like免费版 using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Awake/FixedUpdate', /// 我们使用 'HotUpdateBehaviourFixedUpdate' 来绑定预制体 /// 预制体里设置'Floats': "m_DampTime"/"m_ScreenEdgeBuffer"/"m_MinSize". /// </summary> public class CameraControl : LikeBehaviour //RongRong : 基类'MonoBehaviour'改为'LikeBehaviour' { public float m_DampTime = 0.2f; // Approximate time for the camera to refocus. public float m_ScreenEdgeBuffer = 4f; // Space between the top/bottom most target and the screen edge. public float m_MinSize = 6.5f; // The smallest orthographic size the camera can be. [HideInInspector] public Transform[] m_Targets; // All the targets the camera needs to encompass. private Camera m_Camera; // Used for referencing the camera. private float m_ZoomSpeed; // Reference speed for the smooth damping of the orthographic size. private Vector3 m_MoveVelocity; // Reference velocity for the smooth damping of the position. private Vector3 m_DesiredPosition; // The position the camera is moving towards. private void Awake () { m_Camera = gameObject.GetComponentInChildren<Camera> ();// RongRong : 加前缀 "gameObject." // RongRong : 绑定预制体里的值. m_DampTime = GetFloat("m_DampTime", 0.2f); m_ScreenEdgeBuffer = GetFloat("m_ScreenEdgeBuffer", 4f); m_MinSize = GetFloat("m_MinSize", 6.5f); } private void FixedUpdate () { // Move the camera towards a desired position. Move (); // Change the size of the camera based. Zoom (); } private void Move () { // Find the average position of the targets. FindAveragePosition (); // Smoothly transition to that position. // RongRong : 免费版不支持 'ref',这里只能借助非热更代码来实现 // transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime); SampleHowToUseModifier.currentVelocity = m_MoveVelocity; transform.localPosition = SampleHowToUseModifier.SmoothDamp(transform.position, m_DesiredPosition, m_DampTime); m_MoveVelocity = SampleHowToUseModifier.currentVelocity; } private void FindAveragePosition () { Vector3 averagePos = new Vector3 (); int numTargets = 0; // Go through all the targets and add their positions together. for (int i = 0; i < m_Targets.Length; i++) { // If the target isn't active, go on to the next one. if (!m_Targets[i].gameObject.activeSelf) continue; // Add to the average and increment the number of targets in the average. averagePos += m_Targets[i].position; numTargets++; } // If there are targets divide the sum of the positions by the number of them to find the average. if (numTargets > 0) averagePos /= numTargets; // Keep the same y value. averagePos.y = transform.position.y; // The desired position is the average position; m_DesiredPosition = averagePos; } private void Zoom () { // Find the required size based on the desired position and smoothly transition to that size. float requiredSize = FindRequiredSize(); // RongRong : 免费版不支持 'ref',这里只能借助非热更代码来实现 // m_Camera.orthographicSize = Mathf.SmoothDamp (m_Camera.orthographicSize, requiredSize, ref m_ZoomSpeed, m_DampTime); SampleHowToUseModifier.currentVelocityFloat = m_ZoomSpeed; m_Camera.orthographicSize = SampleHowToUseModifier.SmoothDamp(m_Camera.orthographicSize, requiredSize, m_DampTime); m_ZoomSpeed = SampleHowToUseModifier.currentVelocityFloat; } private float FindRequiredSize () { // Find the position the camera rig is moving towards in its local space. Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition); // Start the camera's size calculation at zero. float size = 0f; // Go through all the targets... for (int i = 0; i < m_Targets.Length; i++) { // ... and if they aren't active continue on to the next target. if (!m_Targets[i].gameObject.activeSelf) continue; // Otherwise, find the position of the target in the camera's local space. Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position); // Find the position of the target from the desired position of the camera's local space. Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos; // Choose the largest out of the current size and the distance of the tank 'up' or 'down' from the camera. size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.y)); // Choose the largest out of the current size and the calculated size based on the tank being to the left or right of the camera. size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.x) / m_Camera.aspect); } // Add the edge buffer to the size. size += m_ScreenEdgeBuffer; // Make sure the camera's size isn't below the minimum. size = Mathf.Max (size, m_MinSize); return size; } public void SetStartPositionAndSize () { // Find the desired position. FindAveragePosition (); // Set the camera's position to the desired position without damping. transform.position = m_DesiredPosition; // Find and set the required size of the camera. m_Camera.orthographicSize = FindRequiredSize (); } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Managers/GameManager.cs',以下是完整版和免费版对应的改后热更新代码:
//C#Like完整版 using System.Collections; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; namespace CSharpLike//RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Start/Awake', /// 我们使用 'HotUpdateBehaviour' 来绑定预制体. /// 预制体设置 'Integers' : "m_NumRoundsToWin" /// 预制体设置 'Floats' : "m_StartDelay"/"m_EndDelay". /// 预制体设置 'Game Objects' : "m_CameraControl"/"m_MessageText"/"m_Tanks0"/"m_Tanks1". /// 预制体设置 'Colors' : "m_Tanks0"/"m_Tanks1". /// </summary> public class GameManager : LikeBehaviour //RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public int m_NumRoundsToWin = 5; // The number of rounds a single player has to win to win the game. public float m_StartDelay = 3f; // The delay between the start of RoundStarting and RoundPlaying phases. public float m_EndDelay = 3f; // The delay between the end of RoundPlaying and RoundEnding phases. public CameraControl m_CameraControl; // Reference to the CameraControl script for control during different phases. public Text m_MessageText; // Reference to the overlay Text to display winning text, etc. public GameObject m_TankPrefab; // Reference to the prefab the players will control. public TankManager[] m_Tanks; // A collection of managers for enabling and disabling different aspects of the tanks. private int m_RoundNumber; // Which round the game is currently on. private WaitForSeconds m_StartWait; // Used to have a delay whilst the round starts. private WaitForSeconds m_EndWait; // Used to have a delay whilst the round or game ends. private TankManager m_RoundWinner; // Reference to the winner of the current round. Used to make an announcement of who won. private TankManager m_GameWinner; // Reference to the winner of the game. Used to make an announcement of who won. const float k_MaxDepenetrationVelocity = float.PositiveInfinity; // RongRong : 绑定必须放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 绑定预制体里的数值. m_NumRoundsToWin = GetInt("m_NumRoundsToWin", 5); m_StartDelay = GetFloat("m_StartDelay", 3f); m_EndDelay = GetFloat("m_EndDelay", 3f); m_MessageText = GetComponent<Text>("m_MessageText"); m_TankPrefab = GetGameObject("m_TankPrefab"); m_Tanks = new TankManager[2]; for (int i = 0; i < 2; i++) { TankManager tankManager = new TankManager(); tankManager.m_SpawnPoint = GetGameObject("m_Tanks"+i).transform; tankManager.m_PlayerColor = GetColor("m_Tanks"+i); m_Tanks[i] = tankManager; } // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 #if UNITY_WEBGL ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/BackgroundMusic.wav", AudioType.WAV, (AudioClip audioClip) => { if (audioClip != null) { gameObject.GetComponent<AudioSource>().clip = audioClip; gameObject.GetComponent<AudioSource>().Play(); } }); #endif } private void Start() { m_CameraControl = HotUpdateBehaviour.GetComponentByType(GetGameObject("m_CameraControl"), typeof(CameraControl)) as CameraControl; // This line fixes a change to the physics engine. Physics.defaultMaxDepenetrationVelocity = k_MaxDepenetrationVelocity; // Create the delays so they only have to be made once. m_StartWait = new WaitForSeconds (m_StartDelay); m_EndWait = new WaitForSeconds (m_EndDelay); SpawnAllTanks(); SetCameraTargets(); // Once the tanks have been created and the camera is using them as targets, start the game. // RongRong : 'StartCoroutine(GameLoop ())' 改为 'StartCoroutine("GameLoop");' StartCoroutine("GameLoop"); } private void SpawnAllTanks() { // For all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... create them, set their player number and references needed for control. // RongRong : 'Instantiate' 改为 'GameObject.Instantiate' m_Tanks[i].m_Instance = GameObject.Instantiate(m_TankPrefab, m_Tanks[i].m_SpawnPoint.position, m_Tanks[i].m_SpawnPoint.rotation) as GameObject; m_Tanks[i].m_PlayerNumber = i + 1; m_Tanks[i].Setup(); } } private void SetCameraTargets() { // Create a collection of transforms the same size as the number of tanks. Transform[] targets = new Transform[m_Tanks.Length]; // For each of these transforms... for (int i = 0; i < targets.Length; i++) { // ... set it to the appropriate tank transform. targets[i] = m_Tanks[i].m_Instance.transform; } // These are the targets the camera should follow. m_CameraControl.m_Targets = targets; } // This is called from start and will run each phase of the game one after another. private IEnumerator GameLoop () { // Start off by running the 'RoundStarting' coroutine but don't return until it's finished. // RongRong : 'StartCoroutine (RoundStarting ())' 改为 'StartCoroutine("RoundStarting")' yield return StartCoroutine("RoundStarting"); // Once the 'RoundStarting' coroutine is finished, run the 'RoundPlaying' coroutine but don't return until it's finished. // RongRong : 'StartCoroutine (RoundPlaying())' 改为 'StartCoroutine("RoundPlaying")' yield return StartCoroutine("RoundPlaying"); // Once execution has returned here, run the 'RoundEnding' coroutine, again don't return until it's finished. // RongRong : 'StartCoroutine (RoundEnding())' 改为 'StartCoroutine("RoundEnding")' yield return StartCoroutine("RoundEnding"); // This code is not run until 'RoundEnding' has finished. At which point, check if a game winner has been found. if (m_GameWinner != null) { // If there is a game winner, restart the level. SceneManager.LoadScene ("_Complete-Game_HotUpdate"); } else { // If there isn't a winner yet, restart this coroutine so the loop continues. // Note that this coroutine doesn't yield. This means that the current version of the GameLoop will end. // RongRong : 'StartCoroutine(GameLoop ())' 改为 'StartCoroutine("GameLoop");' StartCoroutine("GameLoop"); } } private IEnumerator RoundStarting () { // As soon as the round starts reset the tanks and make sure they can't move. ResetAllTanks (); DisableTankControl (); // Snap the camera's zoom and position to something appropriate for the reset tanks. m_CameraControl.SetStartPositionAndSize(); // Increment the round number and display text showing the players what round it is. m_RoundNumber++; m_MessageText.text = "ROUND " + m_RoundNumber; // Wait for the specified length of time until yielding control back to the game loop. yield return m_StartWait; } private IEnumerator RoundPlaying () { // As soon as the round begins playing let the players control the tanks. EnableTankControl (); // Clear the text from the screen. m_MessageText.text = string.Empty; // While there is not one tank left... while (!OneTankLeft()) { // ... return on the next frame. yield return null; } } private IEnumerator RoundEnding () { // Stop tanks from moving. DisableTankControl (); // Clear the winner from the previous round. m_RoundWinner = null; // See if there is a winner now the round is over. m_RoundWinner = GetRoundWinner (); // If there is a winner, increment their score. if (m_RoundWinner != null) m_RoundWinner.m_Wins++; // Now the winner's score has been incremented, see if someone has one the game. m_GameWinner = GetGameWinner (); // Get a message based on the scores and whether or not there is a game winner and display it. string message = EndMessage (); m_MessageText.text = message; // Wait for the specified length of time until yielding control back to the game loop. yield return m_EndWait; } // This is used to check if there is one or fewer tanks remaining and thus the round should end. private bool OneTankLeft() { // Start the count of tanks left at zero. int numTanksLeft = 0; // Go through all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... and if they are active, increment the counter. if (m_Tanks[i].m_Instance.activeSelf) numTanksLeft++; } // If there are one or fewer tanks remaining return true, otherwise return false. return numTanksLeft <= 1; } // This function is to find out if there is a winner of the round. // This function is called with the assumption that 1 or fewer tanks are currently active. private TankManager GetRoundWinner() { // Go through all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... and if one of them is active, it is the winner so return it. if (m_Tanks[i].m_Instance.activeSelf) return m_Tanks[i]; } // If none of the tanks are active it is a draw so return null. return null; } // This function is to find out if there is a winner of the game. private TankManager GetGameWinner() { // Go through all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... and if one of them has enough rounds to win the game, return it. if (m_Tanks[i].m_Wins == m_NumRoundsToWin) return m_Tanks[i]; } // If no tanks have enough rounds to win, return null. return null; } // Returns a string message to display at the end of each round. private string EndMessage() { // By default when a round ends there are no winners so the default end message is a draw. string message = "DRAW!"; // If there is a winner then change the message to reflect that. if (m_RoundWinner != null) message = m_RoundWinner.m_ColoredPlayerText + " WINS THE ROUND!"; // Add some line breaks after the initial message. message += "\n\n\n\n"; // Go through all the tanks and add each of their scores to the message. for (int i = 0; i < m_Tanks.Length; i++) { message += m_Tanks[i].m_ColoredPlayerText + ": " + m_Tanks[i].m_Wins + " WINS\n"; } // If there is a game winner, change the entire message to reflect that. if (m_GameWinner != null) message = m_GameWinner.m_ColoredPlayerText + " WINS THE GAME!"; return message; } // This function is used to turn all the tanks back on and reset their positions and properties. private void ResetAllTanks() { for (int i = 0; i < m_Tanks.Length; i++) { m_Tanks[i].Reset(); } } private void EnableTankControl() { for (int i = 0; i < m_Tanks.Length; i++) { m_Tanks[i].EnableControl(); } } private void DisableTankControl() { for (int i = 0; i < m_Tanks.Length; i++) { m_Tanks[i].DisableControl(); } } } }//C#Like免费版 using System.Collections; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; namespace CSharpLike//RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含 'Start/Update', 我们使用 'Update' 代替协程(因为免费版无法使用协程), /// 我们使用 'HotUpdateBehaviourUpdate' 绑定预制体. /// 预制体设置 'Integers' : "m_NumRoundsToWin". /// 预制体设置 'Floats' : "m_StartDelay"/"m_EndDelay". /// 预制体设置 'Game Objects' : "m_CameraControl"/"m_MessageText"/"m_Tanks0"/"m_Tanks1". /// 预制体设置 'Colors' : "m_Tanks0"/"m_Tanks1". /// </summary> public class GameManager : LikeBehaviour //RongRong : 'MonoBehaviour' 改为 'LikeBehaviour' { public int m_NumRoundsToWin = 5; // The number of rounds a single player has to win to win the game. public float m_StartDelay = 3f; // The delay between the start of RoundStarting and RoundPlaying phases. public float m_EndDelay = 3f; // The delay between the end of RoundPlaying and RoundEnding phases. public CameraControl m_CameraControl; // Reference to the CameraControl script for control during different phases. public Text m_MessageText; // Reference to the overlay Text to display winning text, etc. public GameObject m_TankPrefab; // Reference to the prefab the players will control. public TankManager[] m_Tanks; // A collection of managers for enabling and disabling different aspects of the tanks. private int m_RoundNumber; // Which round the game is currently on. // RongRong : 免费版不支持协程 //private WaitForSeconds m_StartWait; // Used to have a delay whilst the round starts. //private WaitForSeconds m_EndWait; // Used to have a delay whilst the round or game ends. private TankManager m_RoundWinner; // Reference to the winner of the current round. Used to make an announcement of who won. private TankManager m_GameWinner; // Reference to the winner of the game. Used to make an announcement of who won. const float k_MaxDepenetrationVelocity = float.PositiveInfinity; // RongRong : 绑定必须放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 绑定预制体里的值. m_NumRoundsToWin = GetInt("m_NumRoundsToWin", 5); m_StartDelay = GetFloat("m_StartDelay", 3f); m_EndDelay = GetFloat("m_EndDelay", 3f); m_MessageText = GetComponent<Text>("m_MessageText"); m_TankPrefab = GetGameObject("m_TankPrefab"); m_Tanks = new TankManager[2]; for (int i = 0; i < 2; i++) { TankManager tankManager = new TankManager(); tankManager.m_SpawnPoint = GetGameObject("m_Tanks"+i).transform; tankManager.m_PlayerColor = GetColor("m_Tanks"+i); m_Tanks[i] = tankManager; } // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 if (Application.platform == RuntimePlatform.WebGLPlayer) { ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/BackgroundMusic.wav", AudioType.WAV, (AudioClip audioClip) => { if (audioClip != null) { gameObject.GetComponent<AudioSource>().clip = audioClip; gameObject.GetComponent<AudioSource>().Play(); } }); } } private void Start() { m_CameraControl = HotUpdateBehaviour.GetComponentByType(GetGameObject("m_CameraControl"), typeof(CameraControl)) as CameraControl; // This line fixes a change to the physics engine. Physics.defaultMaxDepenetrationVelocity = k_MaxDepenetrationVelocity; // Create the delays so they only have to be made once. // RongRong : 免费版不支持协程 //m_StartWait = new WaitForSeconds (m_StartDelay); //m_EndWait = new WaitForSeconds (m_EndDelay); SpawnAllTanks(); SetCameraTargets(); // Once the tanks have been created and the camera is using them as targets, start the game. mState = 1; } /// <summary> /// RongRong : 我们使用7个状态代替协程 /// 0-未开始; /// 1-回合开始; /// 2-回合开始等待超时; /// 3-回合进入; /// 4-回合循环中; /// 5-回合结束; /// 6-回合结束等待超时; /// </summary> int mState = 0; /// <summary> /// RongRong : 我们使用累计时间代替'm_StartWait'和'm_EndWait' /// </summary> float mDeltaTime = 0f; /// <summary> /// RongRong : 我们使用 Update 代替协程 /// </summary> /// <param name="deltaTime">距离上一次Update的增量时间</param> void Update(float deltaTime) { if (mState == 1)//回合开始 : 刚进入回合开始,做初始化工作 { RoundStarting(); mDeltaTime = 0f; mState = 2; } else if (mState == 2)//回合开始等待超时: 等待超时进入下一个状态 { mDeltaTime += deltaTime; if (mDeltaTime >= m_StartDelay)//超时进入下一状态 { mDeltaTime = 0f; mState = 3; } } else if (mState == 3)//回合进入: 刚进入回合,做初始化工作 { // As soon as the round begins playing let the players control the tanks. EnableTankControl(); // Clear the text from the screen. m_MessageText.text = string.Empty; mState = 4; } else if (mState == 4)//回合循环中: 主循环, 检查回合是否结束. { if (OneTankLeft())//Once just have one tank left, change to next state mState = 5; } else if (mState == 5)//回合结束 : 计算结果 { RoundEnding(); mState = 6; } else if (mState == 6)//回合结束等待超时: 等待超时进入下一状态 { mDeltaTime += deltaTime; if (mDeltaTime >= m_EndDelay)//超时进入下一状态 { // This code is not run until 'RoundEnding' has finished. At which point, check if a game winner has been found. if (m_GameWinner != null) { // If there is a game winner, restart the level. SceneManager.LoadScene("_Complete-Game_HotUpdate"); } else { // If there isn't a winner yet, restart this coroutine so the loop continues. mDeltaTime = 0f; mState = 1; } } } } private void SpawnAllTanks() { // For all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... create them, set their player number and references needed for control. // RongRong : 'Instantiate' 改为 'GameObject.Instantiate' m_Tanks[i].m_Instance = GameObject.Instantiate(m_TankPrefab, m_Tanks[i].m_SpawnPoint.position, m_Tanks[i].m_SpawnPoint.rotation) as GameObject; m_Tanks[i].m_PlayerNumber = i + 1; m_Tanks[i].Setup(); } } private void SetCameraTargets() { // Create a collection of transforms the same size as the number of tanks. Transform[] targets = new Transform[m_Tanks.Length]; // For each of these transforms... for (int i = 0; i < targets.Length; i++) { // ... set it to the appropriate tank transform. targets[i] = m_Tanks[i].m_Instance.transform; } // These are the targets the camera should follow. m_CameraControl.m_Targets = targets; } private void RoundStarting () { // As soon as the round starts reset the tanks and make sure they can't move. ResetAllTanks (); DisableTankControl (); // Snap the camera's zoom and position to something appropriate for the reset tanks. m_CameraControl.SetStartPositionAndSize(); // Increment the round number and display text showing the players what round it is. m_RoundNumber++; m_MessageText.text = "ROUND " + m_RoundNumber; } private void RoundEnding () { // Stop tanks from moving. DisableTankControl (); // Clear the winner from the previous round. m_RoundWinner = null; // See if there is a winner now the round is over. m_RoundWinner = GetRoundWinner (); // If there is a winner, increment their score. if (m_RoundWinner != null) m_RoundWinner.m_Wins++; // Now the winner's score has been incremented, see if someone has one the game. m_GameWinner = GetGameWinner (); // Get a message based on the scores and whether or not there is a game winner and display it. string message = EndMessage (); m_MessageText.text = message; } // This is used to check if there is one or fewer tanks remaining and thus the round should end. private bool OneTankLeft() { // Start the count of tanks left at zero. int numTanksLeft = 0; // Go through all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... and if they are active, increment the counter. if (m_Tanks[i].m_Instance.activeSelf) numTanksLeft++; } // If there are one or fewer tanks remaining return true, otherwise return false. return numTanksLeft <= 1; } // This function is to find out if there is a winner of the round. // This function is called with the assumption that 1 or fewer tanks are currently active. private TankManager GetRoundWinner() { // Go through all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... and if one of them is active, it is the winner so return it. if (m_Tanks[i].m_Instance.activeSelf) return m_Tanks[i]; } // If none of the tanks are active it is a draw so return null. return null; } // This function is to find out if there is a winner of the game. private TankManager GetGameWinner() { // Go through all the tanks... for (int i = 0; i < m_Tanks.Length; i++) { // ... and if one of them has enough rounds to win the game, return it. if (m_Tanks[i].m_Wins == m_NumRoundsToWin) return m_Tanks[i]; } // If no tanks have enough rounds to win, return null. return null; } // Returns a string message to display at the end of each round. private string EndMessage() { // By default when a round ends there are no winners so the default end message is a draw. string message = "DRAW!"; // If there is a winner then change the message to reflect that. if (m_RoundWinner != null) message = m_RoundWinner.m_ColoredPlayerText + " WINS THE ROUND!"; // Add some line breaks after the initial message. message += "\n\n\n\n"; // Go through all the tanks and add each of their scores to the message. for (int i = 0; i < m_Tanks.Length; i++) { message += m_Tanks[i].m_ColoredPlayerText + ": " + m_Tanks[i].m_Wins + " WINS\n"; } // If there is a game winner, change the entire message to reflect that. if (m_GameWinner != null) message = m_GameWinner.m_ColoredPlayerText + " WINS THE GAME!"; return message; } // This function is used to turn all the tanks back on and reset their positions and properties. private void ResetAllTanks() { for (int i = 0; i < m_Tanks.Length; i++) { m_Tanks[i].Reset(); } } private void EnableTankControl() { for (int i = 0; i < m_Tanks.Length; i++) { m_Tanks[i].EnableControl(); } } private void DisableTankControl() { for (int i = 0; i < m_Tanks.Length; i++) { m_Tanks[i].DisableControl(); } } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Managers/TankManager.cs',以下是完整版和免费版对应的改后热更新代码(两者相同):
using System; using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { [Serializable]// RongRong : 不支持 '[Serializable]' 或 '[HideInInspector]', 你可以保留它,但它没任何效果 public class TankManager { // This class is to manage various settings on a tank. // It works with the GameManager class to control how the tanks behave // and whether or not players have control of their tank in the // different phases of the game. public Color m_PlayerColor; // This is the color this tank will be tinted. public Transform m_SpawnPoint; // The position and direction the tank will have when it spawns. [HideInInspector] public int m_PlayerNumber; // This specifies which player this the manager for. [HideInInspector] public string m_ColoredPlayerText; // A string that represents the player with their number colored to match their tank. [HideInInspector] public GameObject m_Instance; // A reference to the instance of the tank when it is created. [HideInInspector] public int m_Wins; // The number of wins this player has so far. private TankMovement m_Movement; // Reference to tank's movement script, used to disable and enable control. private TankShooting m_Shooting; // Reference to tank's shooting script, used to disable and enable control. private GameObject m_CanvasGameObject; // Used to disable the world space UI during the Starting and Ending phases of each round. public void Setup () { // Get references to the components. // RongRong : 'm_Instance.GetComponent<TankMovement> ();' 改为 'HotUpdateBehaviour.GetComponentByType(m_Instance, typeof(TankMovement)) as TankMovement;' m_Movement = HotUpdateBehaviour.GetComponentByType(m_Instance, typeof(TankMovement)) as TankMovement; // RongRong : 'm_Instance.GetComponent<TankShooting> ();' 改为 'HotUpdateBehaviour.GetComponentByType(m_Instance, typeof(TankShooting)) as TankShooting;' m_Shooting = HotUpdateBehaviour.GetComponentByType(m_Instance, typeof(TankShooting)) as TankShooting; m_CanvasGameObject = m_Instance.GetComponentInChildren<Canvas> ().gameObject; // Set the player numbers to be consistent across the scripts. m_Movement.m_PlayerNumber = m_PlayerNumber; m_Shooting.m_PlayerNumber = m_PlayerNumber; // Create a string using the correct color that says 'PLAYER 1' etc based on the tank's color and the player's number. m_ColoredPlayerText = "<color=#" + ColorUtility.ToHtmlStringRGB(m_PlayerColor) + ">PLAYER " + m_PlayerNumber + "</color>"; // Get all of the renderers of the tank. MeshRenderer[] renderers = m_Instance.GetComponentsInChildren<MeshRenderer> (); // Go through all the renderers... for (int i = 0; i < renderers.Length; i++) { // ... set their material color to the color specific to this tank. renderers[i].material.color = m_PlayerColor; } } // Used during the phases of the game where the player shouldn't be able to control their tank. public void DisableControl () { m_Movement.behaviour.enabled = false; m_Shooting.behaviour.enabled = false; m_CanvasGameObject.SetActive (false); } // Used during the phases of the game where the player should be able to control their tank. public void EnableControl () { m_Movement.behaviour.enabled = true; m_Shooting.behaviour.enabled = true; m_CanvasGameObject.SetActive (true); } // Used at the start of each round to put the tank into it's default state. public void Reset () { m_Instance.transform.position = m_SpawnPoint.position; m_Instance.transform.rotation = m_SpawnPoint.rotation; m_Instance.SetActive (false); m_Instance.SetActive (true); } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Shell/ShellExplosion.cs',以下是完整版和免费版对应的改后热更新代码:
//C#Like完整版 using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Start/OnTriggerEnter', /// 我们使用 'HotUpdateBehaviourTrigger' 绑定预制体. /// 预制体设置 'Game Objects' : "m_ExplosionParticles"/"m_ExplosionAudio". /// 预制体设置 'Floats' : "m_MaxDamage"/"m_ExplosionForce"/"m_MaxLifeTime"/"m_ExplosionRadius". /// 预制体设置 'Strings' : "m_TankMask". /// </summary> public class ShellExplosion : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public LayerMask m_TankMask; // Used to filter what the explosion affects, this should be set to "Players". public ParticleSystem m_ExplosionParticles; // Reference to the particles that will play on explosion. public AudioSource m_ExplosionAudio; // Reference to the audio that will play on explosion. public float m_MaxDamage = 100f; // The amount of damage done if the explosion is centred on a tank. public float m_ExplosionForce = 1000f; // The amount of force added to a tank at the centre of the explosion. public float m_MaxLifeTime = 2f; // The time in seconds before the shell is removed. public float m_ExplosionRadius = 5f; // The maximum distance away from the explosion tanks can be and are still affected. // RongRong : 绑定数值必须在放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 从预制体里绑定数值 m_MaxDamage = GetFloat("m_MaxDamage", 100f); m_ExplosionForce = GetFloat("m_ExplosionForce", 1000f); m_MaxLifeTime = GetFloat("m_MaxLifeTime", 2f); m_ExplosionRadius = GetFloat("m_ExplosionRadius", 5f); m_TankMask = LayerMask.NameToLayer(GetString("m_TankMask", "Players"));// RongRong : 不支持绑定结构体,我们使用字符串来绑定,然后转回结构体 m_ExplosionParticles = GetComponent<ParticleSystem>("m_ExplosionParticles"); m_ExplosionAudio = GetComponent<AudioSource>("m_ExplosionAudio"); #if UNITY_WEBGL ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/ShellExplosion.wav", AudioType.WAV, (AudioClip audioClip) => { m_ExplosionAudio.clip = audioClip; }); #endif } private void Start () { // If it isn't destroyed by then, destroy the shell after it's lifetime. // RongRong : 加前缀 "GameObject." GameObject.Destroy (gameObject, m_MaxLifeTime); } private void OnTriggerEnter (Collider other) { // Collect all the colliders in a sphere from the shell's current position to a radius of the explosion radius. // RongRong : 'm_TankMask' 改为 '1 << m_TankMask.value'. 调试模式下数值为 512(等于 1 << 9). Collider[] colliders = Physics.OverlapSphere (transform.position, m_ExplosionRadius, 512); // Go through all the colliders... for (int i = 0; i < colliders.Length; i++) { // ... and find their rigidbody. Rigidbody targetRigidbody = colliders[i].GetComponent<Rigidbody> (); // If they don't have a rigidbody, go on to the next collider. if (targetRigidbody == null) continue; // Add an explosion force. targetRigidbody.AddExplosionForce (m_ExplosionForce, transform.position, m_ExplosionRadius); // Find the TankHealth script associated with the rigidbody. // RongRong : 'targetRigidbody.GetComponent<TankHealth> ();' 改为 'HotUpdateBehaviour.GetComponentByType(targetRigidbody, typeof(TankHealth)) as TankHealth;' TankHealth targetHealth = HotUpdateBehaviour.GetComponentByType(targetRigidbody, typeof(TankHealth)) as TankHealth; // If there is no TankHealth script attached to the gameobject, go on to the next collider. // RongRong : '!targetHealth' 改为 'targetHealth == null' if (targetHealth == null) continue; // Calculate the amount of damage the target should take based on it's distance from the shell. float damage = CalculateDamage (targetRigidbody.position); // Deal this damage to the tank. targetHealth.TakeDamage (damage); } // Unparent the particles from the shell. m_ExplosionParticles.transform.parent = null; // Play the particle system. m_ExplosionParticles.Play(); // Play the explosion sound effect. m_ExplosionAudio.Play(); // Once the particles have finished, destroy the gameobject they are on. ParticleSystem.MainModule mainModule = m_ExplosionParticles.main; // RongRong : 加前缀 "GameObject." GameObject.Destroy (m_ExplosionParticles.gameObject, mainModule.duration); // Destroy the shell. // RongRong : 加前缀 "GameObject." GameObject.Destroy (gameObject); } private float CalculateDamage (Vector3 targetPosition) { // Create a vector from the shell to the target. Vector3 explosionToTarget = targetPosition - transform.position; // Calculate the distance from the shell to the target. float explosionDistance = explosionToTarget.magnitude; // Calculate the proportion of the maximum distance (the explosionRadius) the target is away. float relativeDistance = (m_ExplosionRadius - explosionDistance) / m_ExplosionRadius; // Calculate damage as this proportion of the maximum possible damage. float damage = relativeDistance * m_MaxDamage; // Make sure that the minimum damage is always 0. damage = Mathf.Max (0f, damage); return damage; } } }//C#Like免费版 using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Start/OnTriggerEnter', /// 使用 'HotUpdateBehaviourTrigger' 绑定预制体. /// 预制体设置 'Game Objects' : "m_ExplosionParticles"/"m_ExplosionAudio". /// 预制体设置 'Floats' : "m_MaxDamage"/"m_ExplosionForce"/"m_MaxLifeTime"/"m_ExplosionRadius". /// 预制体设置 'Strings' : "m_TankMask". /// </summary> public class ShellExplosion : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public LayerMask m_TankMask; // Used to filter what the explosion affects, this should be set to "Players". public ParticleSystem m_ExplosionParticles; // Reference to the particles that will play on explosion. public AudioSource m_ExplosionAudio; // Reference to the audio that will play on explosion. public float m_MaxDamage = 100f; // The amount of damage done if the explosion is centred on a tank. public float m_ExplosionForce = 1000f; // The amount of force added to a tank at the centre of the explosion. public float m_MaxLifeTime = 2f; // The time in seconds before the shell is removed. public float m_ExplosionRadius = 5f; // The maximum distance away from the explosion tanks can be and are still affected. // RongRong : 绑定数值必须在放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 绑定预制体数值 m_MaxDamage = GetFloat("m_MaxDamage", 100f); m_ExplosionForce = GetFloat("m_ExplosionForce", 1000f); m_MaxLifeTime = GetFloat("m_MaxLifeTime", 2f); m_ExplosionRadius = GetFloat("m_ExplosionRadius", 5f); m_TankMask = LayerMask.NameToLayer(GetString("m_TankMask", "Players"));// RongRong : 不支持绑定结构体,我们用字符串代替,然后转回结构体 m_ExplosionParticles = GetComponent<ParticleSystem>("m_ExplosionParticles"); m_ExplosionAudio = GetComponent<AudioSource>("m_ExplosionAudio"); // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 if (Application.platform == RuntimePlatform.WebGLPlayer) { ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/ShellExplosion.wav", AudioType.WAV, (AudioClip audioClip) => { m_ExplosionAudio.clip = audioClip; }); } } private void Start () { // If it isn't destroyed by then, destroy the shell after it's lifetime. // RongRong : 加前缀 "GameObject." GameObject.Destroy (gameObject, m_MaxLifeTime); } private void OnTriggerEnter (Collider other) { // Collect all the colliders in a sphere from the shell's current position to a radius of the explosion radius. // RongRong : 'm_TankMask' 改为 '512'. 调试数值为 512(等于 1 << m_TankMask.value). Collider[] colliders = Physics.OverlapSphere (transform.position, m_ExplosionRadius, 512); // Go through all the colliders... for (int i = 0; i < colliders.Length; i++) { // ... and find their rigidbody. Rigidbody targetRigidbody = colliders[i].GetComponent<Rigidbody> (); // If they don't have a rigidbody, go on to the next collider. if (targetRigidbody == null) continue; // Add an explosion force. targetRigidbody.AddExplosionForce (m_ExplosionForce, transform.position, m_ExplosionRadius); // Find the TankHealth script associated with the rigidbody. // RongRong : 'targetRigidbody.GetComponent<TankHealth> ();' 改为 'HotUpdateBehaviour.GetComponentByType(targetRigidbody, typeof(TankHealth)) as TankHealth;' TankHealth targetHealth = HotUpdateBehaviour.GetComponentByType(targetRigidbody, typeof(TankHealth)) as TankHealth; // If there is no TankHealth script attached to the gameobject, go on to the next collider. // RongRong : '!targetHealth' 改为 'targetHealth == null' if (targetHealth == null) continue; // Calculate the amount of damage the target should take based on it's distance from the shell. float damage = CalculateDamage (targetRigidbody.position); // Deal this damage to the tank. targetHealth.TakeDamage (damage); } // Unparent the particles from the shell. m_ExplosionParticles.transform.parent = null; // Play the particle system. m_ExplosionParticles.Play(); // Play the explosion sound effect. m_ExplosionAudio.Play(); // Once the particles have finished, destroy the gameobject they are on. ParticleSystem.MainModule mainModule = m_ExplosionParticles.main; // RongRong : 添加前缀 "GameObject." GameObject.Destroy (m_ExplosionParticles.gameObject, mainModule.duration); // Destroy the shell. // RongRong : 添加前缀 "GameObject." GameObject.Destroy (gameObject); } private float CalculateDamage (Vector3 targetPosition) { // Create a vector from the shell to the target. Vector3 explosionToTarget = targetPosition - transform.position; // Calculate the distance from the shell to the target. float explosionDistance = explosionToTarget.magnitude; // Calculate the proportion of the maximum distance (the explosionRadius) the target is away. float relativeDistance = (m_ExplosionRadius - explosionDistance) / m_ExplosionRadius; // Calculate damage as this proportion of the maximum possible damage. float damage = relativeDistance * m_MaxDamage; // Make sure that the minimum damage is always 0. damage = Mathf.Max (0f, damage); return damage; } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Shell/TankHealth.cs',以下是完整版和免费版对应的改后热更新代码:
//C#Like完整版 using UnityEngine; using UnityEngine.UI; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Awake/OnEnable', /// we using 'HotUpdateBehaviour' to bind prefabe. /// We add 'Floats' name "m_StartingHealth" in prefab. /// We add 'Colors' name "m_FullHealthColor"/"m_ZeroHealthColor" in prefab. /// We add 'Game Objects' name "m_Slider"/"m_FillImage"/"m_ExplosionPrefab" in prefab. /// </summary> public class TankHealth : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public float m_StartingHealth = 100f; // The amount of health each tank starts with. public Slider m_Slider; // The slider to represent how much health the tank currently has. public Image m_FillImage; // The image component of the slider. public Color m_FullHealthColor = Color.green; // The color the health bar will be when on full health. public Color m_ZeroHealthColor = Color.red; // The color the health bar will be when on no health. public GameObject m_ExplosionPrefab; // A prefab that will be instantiated in Awake, then used whenever the tank dies. private AudioSource m_ExplosionAudio; // The audio source to play when the tank explodes. private ParticleSystem m_ExplosionParticles; // The particle system the will play when the tank is destroyed. private float m_CurrentHealth; // How much health the tank currently has. private bool m_Dead; // Has the tank been reduced beyond zero health yet? private void Awake () { // RongRong : 绑定预制体的数值. m_StartingHealth = GetFloat("m_StartingHealth", 100f); m_Slider = GetComponent<Slider>("m_Slider"); m_FillImage = GetComponent<Image>("m_FillImage"); m_FullHealthColor = GetColor("m_FullHealthColor"); m_ZeroHealthColor = GetColor("m_ZeroHealthColor"); m_ExplosionPrefab = GetGameObject("m_ExplosionPrefab"); // Instantiate the explosion prefab and get a reference to the particle system on it. // RongRong : 'Instantiate' 改为 'GameObject.Instantiate' GameObject go = GameObject.Instantiate(m_ExplosionPrefab) as GameObject; m_ExplosionParticles = go.GetComponent<ParticleSystem> (); // Get a reference to the audio source on the instantiated prefab. m_ExplosionAudio = m_ExplosionParticles.GetComponent<AudioSource> (); // Disable the prefab so it can be activated when it's required. m_ExplosionParticles.gameObject.SetActive (false); // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 #if UNITY_WEBGL ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/TankExplosion.wav", AudioType.WAV, (AudioClip audioClip) => { m_ExplosionAudio.clip = audioClip; }); #endif } private void OnEnable() { // When the tank is enabled, reset the tank's health and whether or not it's dead. m_CurrentHealth = m_StartingHealth; m_Dead = false; // Update the health slider's value and color. SetHealthUI(); } public void TakeDamage (float amount) { // Reduce current health by the amount of damage done. m_CurrentHealth -= amount; // Change the UI elements appropriately. SetHealthUI (); // If the current health is at or below zero and it has not yet been registered, call OnDeath. if (m_CurrentHealth <= 0f && !m_Dead) { OnDeath (); } } private void SetHealthUI () { // Set the slider's value appropriately. m_Slider.value = m_CurrentHealth; // Interpolate the color of the bar between the choosen colours based on the current percentage of the starting health. m_FillImage.color = Color.Lerp (m_ZeroHealthColor, m_FullHealthColor, m_CurrentHealth / m_StartingHealth); } private void OnDeath () { // Set the flag so that this function is only called once. m_Dead = true; // Move the instantiated explosion prefab to the tank's position and turn it on. m_ExplosionParticles.transform.position = transform.position; m_ExplosionParticles.gameObject.SetActive (true); // Play the particle system of the tank exploding. m_ExplosionParticles.Play (); // Play the tank explosion sound effect. m_ExplosionAudio.Play(); // Turn the tank off. gameObject.SetActive (false); } } } //C#Like免费版 using UnityEngine; using UnityEngine.UI; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Awake/OnEnable', /// 我们使用 'HotUpdateBehaviour' 绑定数据. /// 预制体设置 'Floats' : "m_StartingHealth". /// 预制体设置 'Colors' : "m_FullHealthColor"/"m_ZeroHealthColor". /// 预制体设置 'Game Objects' : "m_Slider"/"m_FillImage"/"m_ExplosionPrefab". /// </summary> public class TankHealth : LikeBehaviour // RongRong : Change 'MonoBehaviour' to 'LikeBehaviour' { public float m_StartingHealth = 100f; // The amount of health each tank starts with. public Slider m_Slider; // The slider to represent how much health the tank currently has. public Image m_FillImage; // The image component of the slider. public Color m_FullHealthColor = Color.green; // The color the health bar will be when on full health. public Color m_ZeroHealthColor = Color.red; // The color the health bar will be when on no health. public GameObject m_ExplosionPrefab; // A prefab that will be instantiated in Awake, then used whenever the tank dies. private AudioSource m_ExplosionAudio; // The audio source to play when the tank explodes. private ParticleSystem m_ExplosionParticles; // The particle system the will play when the tank is destroyed. private float m_CurrentHealth; // How much health the tank currently has. private bool m_Dead; // Has the tank been reduced beyond zero health yet? private void Awake () { // RongRong : 绑定预制体数据 m_StartingHealth = GetFloat("m_StartingHealth", 100f); m_Slider = GetComponent<Slider>("m_Slider"); m_FillImage = GetComponent<Image>("m_FillImage"); m_FullHealthColor = GetColor("m_FullHealthColor"); m_ZeroHealthColor = GetColor("m_ZeroHealthColor"); m_ExplosionPrefab = GetGameObject("m_ExplosionPrefab"); // Instantiate the explosion prefab and get a reference to the particle system on it. // RongRong : 'Instantiate' 改为 'GameObject.Instantiate' GameObject go = GameObject.Instantiate(m_ExplosionPrefab) as GameObject; m_ExplosionParticles = go.GetComponent<ParticleSystem> (); // Get a reference to the audio source on the instantiated prefab. m_ExplosionAudio = m_ExplosionParticles.GetComponent<AudioSource> (); // Disable the prefab so it can be activated when it's required. m_ExplosionParticles.gameObject.SetActive (false); // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 if (Application.platform == RuntimePlatform.WebGLPlayer) { ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/TankExplosion.wav", AudioType.WAV, (AudioClip audioClip) => { m_ExplosionAudio.clip = audioClip; }); } } private void OnEnable() { // When the tank is enabled, reset the tank's health and whether or not it's dead. m_CurrentHealth = m_StartingHealth; m_Dead = false; // Update the health slider's value and color. SetHealthUI(); } public void TakeDamage (float amount) { // Reduce current health by the amount of damage done. m_CurrentHealth -= amount; // Change the UI elements appropriately. SetHealthUI (); // If the current health is at or below zero and it has not yet been registered, call OnDeath. if (m_CurrentHealth <= 0f && !m_Dead) { OnDeath (); } } private void SetHealthUI () { // Set the slider's value appropriately. m_Slider.value = m_CurrentHealth; // Interpolate the color of the bar between the choosen colours based on the current percentage of the starting health. m_FillImage.color = Color.Lerp (m_ZeroHealthColor, m_FullHealthColor, m_CurrentHealth / m_StartingHealth); } private void OnDeath () { // Set the flag so that this function is only called once. m_Dead = true; // Move the instantiated explosion prefab to the tank's position and turn it on. m_ExplosionParticles.transform.position = transform.position; m_ExplosionParticles.gameObject.SetActive (true); // Play the particle system of the tank exploding. m_ExplosionParticles.Play (); // Play the tank explosion sound effect. m_ExplosionAudio.Play(); // Turn the tank off. gameObject.SetActive (false); } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Shell/TankMovement.cs',以下是完整版和免费版对应的改后热更新代码:
//C#Like完整版 using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Awake/OnEnable/OnDisable/Start/Update/FixedUpdate', /// 我们使用 'HotUpdateBehaviourCommon' 绑定预制体, 设置 'scriptUpdateFPS' 数值为 10000, 设置 'enableFixedUpdate' 数值为 true. /// 预制体设置 'Integers' : "m_PlayerNumber". /// 预制体设置 'Floats' : "m_Speed"/"m_TurnSpeed"/"m_PitchRange". /// 预制体设置 'Audio Clips' : "m_EngineIdling"/"m_EngineDriving". /// 预制体设置 'Game Objects' : "m_MovementAudio". /// </summary> public class TankMovement : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public int m_PlayerNumber = 1; // Used to identify which tank belongs to which player. This is set by this tank's manager. public float m_Speed = 12f; // How fast the tank moves forward and back. public float m_TurnSpeed = 180f; // How fast the tank turns in degrees per second. public AudioSource m_MovementAudio; // Reference to the audio source used to play engine sounds. NB: different to the shooting audio source. public AudioClip m_EngineIdling; // Audio to play when the tank isn't moving. public AudioClip m_EngineDriving; // Audio to play when the tank is moving. public float m_PitchRange = 0.2f; // The amount by which the pitch of the engine noises can vary. private string m_MovementAxisName; // The name of the input axis for moving forward and back. private string m_TurnAxisName; // The name of the input axis for turning. private Rigidbody m_Rigidbody; // Reference used to move the tank. private float m_MovementInputValue; // The current value of the movement input. private float m_TurnInputValue; // The current value of the turn input. private float m_OriginalPitch; // The pitch of the audio source at the start of the scene. private ParticleSystem[] m_particleSystems; // References to all the particles systems used by the Tanks private void Awake () { // RongRong : 'GetComponent<Rigidbody> ();' 改为 'gameObject.GetComponent<Rigidbody>();' m_Rigidbody = gameObject.GetComponent<Rigidbody>(); // RongRong : 绑定预制体数值. m_PlayerNumber = GetInt("m_PlayerNumber", 1); m_Speed = GetFloat("m_Speed", 12f); m_TurnSpeed = GetFloat("m_TurnSpeed", 180f); m_PitchRange = GetFloat("m_PitchRange", 0.2f); m_MovementAudio = GetComponent<AudioSource>("m_MovementAudio"); // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 #if UNITY_WEBGL ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/EngineIdle.wav", AudioType.WAV, (AudioClip audioClip) => { m_EngineIdling = audioClip; }); ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/EngineDriving.wav", AudioType.WAV, (AudioClip audioClip) => { m_EngineDriving = audioClip; }); #else m_EngineIdling = GetAudioClip("m_EngineIdling"); m_EngineDriving = GetAudioClip("m_EngineDriving"); #endif } private void OnEnable () { // When the tank is turned on, make sure it's not kinematic. m_Rigidbody.isKinematic = false; // Also reset the input values. m_MovementInputValue = 0f; m_TurnInputValue = 0f; // We grab all the Particle systems child of that Tank to be able to Stop/Play them on Deactivate/Activate // It is needed because we move the Tank when spawning it, and if the Particle System is playing while we do that // it "think" it move from (0,0,0) to the spawn point, creating a huge trail of smoke // RongRong : 'GetComponentsInChildren<ParticleSystem>();' 改为 'gameObject.GetComponentsInChildren<ParticleSystem>();' m_particleSystems = gameObject.GetComponentsInChildren<ParticleSystem>(); for (int i = 0; i < m_particleSystems.Length; ++i) { m_particleSystems[i].Play(); } } private void OnDisable () { // When the tank is turned off, set it to kinematic so it stops moving. m_Rigidbody.isKinematic = true; // Stop all particle system so it "reset" it's position to the actual one instead of thinking we moved when spawning for(int i = 0; i < m_particleSystems.Length; ++i) { m_particleSystems[i].Stop(); } } private void Start () { // The axes names are based on player number. m_MovementAxisName = "Vertical" + m_PlayerNumber; m_TurnAxisName = "Horizontal" + m_PlayerNumber; // Store the original pitch of the audio source. m_OriginalPitch = m_MovementAudio.pitch; } private void Update () { // Store the value of both input axes. m_MovementInputValue = Input.GetAxis (m_MovementAxisName); m_TurnInputValue = Input.GetAxis (m_TurnAxisName); EngineAudio (); } private void EngineAudio () { // If there is no input (the tank is stationary)... if (Mathf.Abs (m_MovementInputValue) < 0.1f && Mathf.Abs (m_TurnInputValue) < 0.1f) { // ... and if the audio source is currently playing the driving clip... if (m_MovementAudio.clip == m_EngineDriving) { // ... change the clip to idling and play it. m_MovementAudio.clip = m_EngineIdling; m_MovementAudio.pitch = Random.Range (m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange); m_MovementAudio.Play (); } } else { // Otherwise if the tank is moving and if the idling clip is currently playing... if (m_MovementAudio.clip == m_EngineIdling) { // ... change the clip to driving and play. m_MovementAudio.clip = m_EngineDriving; m_MovementAudio.pitch = Random.Range(m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange); m_MovementAudio.Play(); } } } private void FixedUpdate () { // Adjust the rigidbodies position and orientation in FixedUpdate. Move (); Turn (); } private void Move () { // Create a vector in the direction the tank is facing with a magnitude based on the input, speed and the time between frames. Vector3 movement = transform.forward * m_MovementInputValue * m_Speed * Time.deltaTime; // Apply this movement to the rigidbody's position. m_Rigidbody.MovePosition(m_Rigidbody.position + movement); } private void Turn () { // Determine the number of degrees to be turned based on the input, speed and time between frames. float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime; // Make this into a rotation in the y axis. Quaternion turnRotation = Quaternion.Euler (0f, turn, 0f); // Apply this rotation to the rigidbody's rotation. m_Rigidbody.MoveRotation (m_Rigidbody.rotation * turnRotation); } } }//C#Like免费版 using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Awake/OnEnable/OnDisable/Start/Update/FixedUpdate', /// 我们使用 'HotUpdateBehaviourCommon' 来绑定预制体, 设置 'scriptUpdateFPS' 数值为 10000, 设置 'enableFixedUpdate' 数值为 true. /// 预制体设置 'Integers' : "m_PlayerNumber". /// 预制体设置 'Floats' : "m_Speed"/"m_TurnSpeed"/"m_PitchRange". /// 预制体设置 'Audio Clips' : "m_EngineIdling"/"m_EngineDriving". /// 预制体设置 'Game Objects' : "m_MovementAudio". /// </summary> public class TankMovement : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public int m_PlayerNumber = 1; // Used to identify which tank belongs to which player. This is set by this tank's manager. public float m_Speed = 12f; // How fast the tank moves forward and back. public float m_TurnSpeed = 180f; // How fast the tank turns in degrees per second. public AudioSource m_MovementAudio; // Reference to the audio source used to play engine sounds. NB: different to the shooting audio source. public AudioClip m_EngineIdling; // Audio to play when the tank isn't moving. public AudioClip m_EngineDriving; // Audio to play when the tank is moving. public float m_PitchRange = 0.2f; // The amount by which the pitch of the engine noises can vary. private string m_MovementAxisName; // The name of the input axis for moving forward and back. private string m_TurnAxisName; // The name of the input axis for turning. private Rigidbody m_Rigidbody; // Reference used to move the tank. private float m_MovementInputValue; // The current value of the movement input. private float m_TurnInputValue; // The current value of the turn input. private float m_OriginalPitch; // The pitch of the audio source at the start of the scene. private ParticleSystem[] m_particleSystems; // References to all the particles systems used by the Tanks private void Awake () { // RongRong : 'GetComponent<Rigidbody> ();' 改为 'gameObject.GetComponent<Rigidbody>();' m_Rigidbody = gameObject.GetComponent<Rigidbody>(); // RongRong : 绑定预制体 m_PlayerNumber = GetInt("m_PlayerNumber", 1); m_Speed = GetFloat("m_Speed", 12f); m_TurnSpeed = GetFloat("m_TurnSpeed", 180f); m_PitchRange = GetFloat("m_PitchRange", 0.2f); m_MovementAudio = GetComponent<AudioSource>("m_MovementAudio"); // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 if (Application.platform == RuntimePlatform.WebGLPlayer) { ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/EngineIdle.wav", AudioType.WAV, (AudioClip audioClip) => { m_EngineIdling = audioClip; }); ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/EngineDriving.wav", AudioType.WAV, (AudioClip audioClip) => { m_EngineDriving = audioClip; }); } else { m_EngineIdling = GetAudioClip("m_EngineIdling"); m_EngineDriving = GetAudioClip("m_EngineDriving"); } } private void OnEnable () { // When the tank is turned on, make sure it's not kinematic. m_Rigidbody.isKinematic = false; // Also reset the input values. m_MovementInputValue = 0f; m_TurnInputValue = 0f; // We grab all the Particle systems child of that Tank to be able to Stop/Play them on Deactivate/Activate // It is needed because we move the Tank when spawning it, and if the Particle System is playing while we do that // it "think" it move from (0,0,0) to the spawn point, creating a huge trail of smoke // RongRong : 'GetComponentsInChildren<ParticleSystem>();' 改为 'gameObject.GetComponentsInChildren<ParticleSystem>();' m_particleSystems = gameObject.GetComponentsInChildren<ParticleSystem>(); for (int i = 0; i < m_particleSystems.Length; ++i) { m_particleSystems[i].Play(); } } private void OnDisable () { // When the tank is turned off, set it to kinematic so it stops moving. m_Rigidbody.isKinematic = true; // Stop all particle system so it "reset" it's position to the actual one instead of thinking we moved when spawning for(int i = 0; i < m_particleSystems.Length; ++i) { m_particleSystems[i].Stop(); } } private void Start () { // The axes names are based on player number. m_MovementAxisName = "Vertical" + m_PlayerNumber; m_TurnAxisName = "Horizontal" + m_PlayerNumber; // Store the original pitch of the audio source. m_OriginalPitch = m_MovementAudio.pitch; } private void Update () { // Store the value of both input axes. m_MovementInputValue = Input.GetAxis (m_MovementAxisName); m_TurnInputValue = Input.GetAxis (m_TurnAxisName); EngineAudio (); } private void EngineAudio () { // If there is no input (the tank is stationary)... if (Mathf.Abs (m_MovementInputValue) < 0.1f && Mathf.Abs (m_TurnInputValue) < 0.1f) { // ... and if the audio source is currently playing the driving clip... if (m_MovementAudio.clip == m_EngineDriving) { // ... change the clip to idling and play it. m_MovementAudio.clip = m_EngineIdling; m_MovementAudio.pitch = Random.Range (m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange); m_MovementAudio.Play (); } } else { // Otherwise if the tank is moving and if the idling clip is currently playing... if (m_MovementAudio.clip == m_EngineIdling) { // ... change the clip to driving and play. m_MovementAudio.clip = m_EngineDriving; m_MovementAudio.pitch = Random.Range(m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange); m_MovementAudio.Play(); } } } private void FixedUpdate () { // Adjust the rigidbodies position and orientation in FixedUpdate. Move (); Turn (); } private void Move () { // Create a vector in the direction the tank is facing with a magnitude based on the input, speed and the time between frames. Vector3 movement = transform.forward * m_MovementInputValue * m_Speed * Time.deltaTime; // Apply this movement to the rigidbody's position. m_Rigidbody.MovePosition(m_Rigidbody.position + movement); } private void Turn () { // Determine the number of degrees to be turned based on the input, speed and time between frames. float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime; // Make this into a rotation in the y axis. Quaternion turnRotation = Quaternion.Euler (0f, turn, 0f); // Apply this rotation to the rigidbody's rotation. m_Rigidbody.MoveRotation (m_Rigidbody.rotation * turnRotation); } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Shell/TankMovement.cs',以下是完整版和免费版对应的改后热更新代码:
//C#Like完整版 using UnityEngine; using UnityEngine.UI; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Start/OnEnable/Update', /// 我们使用 'HotUpdateBehaviourUpdate' 来绑定预制体. /// 预制体设置 'Integers' : "m_PlayerNumber" /// 预制体设置 'Game Objects' : "m_Shell"/"m_FireTransform"/"m_AimSlider"/"m_ShootingAudio" /// 预制体设置 'Floats' : "m_MinLaunchForce"/"m_MaxLaunchForce"/"m_MaxChargeTime" /// 预制体设置 'Audio Clips' : "m_ChargingClip"/"m_FireClip" /// </summary> public class TankShooting : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public int m_PlayerNumber = 1; // Used to identify the different players. public Rigidbody m_Shell; // Prefab of the shell. public Transform m_FireTransform; // A child of the tank where the shells are spawned. public Slider m_AimSlider; // A child of the tank that displays the current launch force. public AudioSource m_ShootingAudio; // Reference to the audio source used to play the shooting audio. NB: different to the movement audio source. public AudioClip m_ChargingClip; // Audio that plays when each shot is charging up. public AudioClip m_FireClip; // Audio that plays when each shot is fired. public float m_MinLaunchForce = 15f; // The force given to the shell if the fire button is not held. public float m_MaxLaunchForce = 30f; // The force given to the shell if the fire button is held for the max charge time. public float m_MaxChargeTime = 0.75f; // How long the shell can charge for before it is fired at max force. private string m_FireButton; // The input axis that is used for launching shells. private float m_CurrentLaunchForce; // The force that will be given to the shell when the fire button is released. private float m_ChargeSpeed; // How fast the launch force increases, based on the max charge time. private bool m_Fired; // Whether or not the shell has been launched with this button press. // RongRong : 绑定数值必须在放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 绑定预制体数值 m_PlayerNumber = GetInt("m_PlayerNumber", 1); m_Shell = GetComponent<Rigidbody>("m_Shell"); m_MinLaunchForce = GetFloat("m_MinLaunchForce", 15f); m_AimSlider = GetComponent<Slider>("m_AimSlider"); m_MaxLaunchForce = GetFloat("m_MaxLaunchForce", 30f); m_MaxChargeTime = GetFloat("m_MaxChargeTime", 0.75f); m_FireTransform = GetComponent<Transform>("m_FireTransform"); m_ShootingAudio = GetComponent<AudioSource>("m_ShootingAudio"); // RongRong : 这个仅仅WebGL有效,因为仅仅WebGL平台下音效报错 #if UNITY_WEBGL ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/ShotCharging.wav", AudioType.WAV, (AudioClip audioClip) => { m_ChargingClip = audioClip; }); ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeDemo/AssetBundles/Tank/WebGL/ShotFiring.wav", AudioType.WAV, (AudioClip audioClip) => { m_FireClip = audioClip; }); #else m_ChargingClip = GetAudioClip("m_ChargingClip"); m_FireClip = GetAudioClip("m_FireClip"); #endif } private void OnEnable() { // When the tank is turned on, reset the launch force and the UI m_CurrentLaunchForce = m_MinLaunchForce; m_AimSlider.value = m_MinLaunchForce; } private void Start () { // The fire axis is based on the player number. m_FireButton = "Fire" + m_PlayerNumber; // The rate that the launch force charges up is the range of possible forces by the max charge time. m_ChargeSpeed = (m_MaxLaunchForce - m_MinLaunchForce) / m_MaxChargeTime; } private void Update () { // The slider should have a default value of the minimum launch force. m_AimSlider.value = m_MinLaunchForce; // If the max force has been exceeded and the shell hasn't yet been launched... if (m_CurrentLaunchForce >= m_MaxLaunchForce && !m_Fired) { // ... use the max force and launch the shell. m_CurrentLaunchForce = m_MaxLaunchForce; Fire (); } // Otherwise, if the fire button has just started being pressed... else if (Input.GetButtonDown (m_FireButton)) { // ... reset the fired flag and reset the launch force. m_Fired = false; m_CurrentLaunchForce = m_MinLaunchForce; // Change the clip to the charging clip and start it playing. m_ShootingAudio.clip = m_ChargingClip; m_ShootingAudio.Play (); } // Otherwise, if the fire button is being held and the shell hasn't been launched yet... else if (Input.GetButton (m_FireButton) && !m_Fired) { // Increment the launch force and update the slider. m_CurrentLaunchForce += m_ChargeSpeed * Time.deltaTime; m_AimSlider.value = m_CurrentLaunchForce; } // Otherwise, if the fire button is released and the shell hasn't been launched yet... else if (Input.GetButtonUp (m_FireButton) && !m_Fired) { // ... launch the shell. Fire (); } } private void Fire () { // Set the fired flag so only Fire is only called once. m_Fired = true; // Create an instance of the shell and store a reference to it's rigidbody. // RongRong : 'Instantiate' 改为 'GameObject.Instantiate' Rigidbody shellInstance = GameObject.Instantiate (m_Shell, m_FireTransform.position, m_FireTransform.rotation) as Rigidbody; // Set the shell's velocity to the launch force in the fire position's forward direction. shellInstance.velocity = m_CurrentLaunchForce * m_FireTransform.forward; // Change the clip to the firing clip and play it. m_ShootingAudio.clip = m_FireClip; m_ShootingAudio.Play (); // Reset the launch force. This is a precaution in case of missing button events. m_CurrentLaunchForce = m_MinLaunchForce; } } }//C#Like免费版 using UnityEngine; using UnityEngine.UI; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Start/OnEnable/Update', /// 我们使用 'HotUpdateBehaviourUpdate' 来绑定预制体. /// 预制体设置 'Integers' : "m_PlayerNumber" /// 预制体设置 'Game Objects' : "m_Shell"/"m_FireTransform"/"m_AimSlider"/"m_ShootingAudio" /// 预制体设置 'Floats' : "m_MinLaunchForce"/"m_MaxLaunchForce"/"m_MaxChargeTime" /// 预制体设置 'Audio Clips' : "m_ChargingClip"/"m_FireClip" /// </summary> public class TankShooting : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { public int m_PlayerNumber = 1; // Used to identify the different players. public Rigidbody m_Shell; // Prefab of the shell. public Transform m_FireTransform; // A child of the tank where the shells are spawned. public Slider m_AimSlider; // A child of the tank that displays the current launch force. public AudioSource m_ShootingAudio; // Reference to the audio source used to play the shooting audio. NB: different to the movement audio source. public AudioClip m_ChargingClip; // Audio that plays when each shot is charging up. public AudioClip m_FireClip; // Audio that plays when each shot is fired. public float m_MinLaunchForce = 15f; // The force given to the shell if the fire button is not held. public float m_MaxLaunchForce = 30f; // The force given to the shell if the fire button is held for the max charge time. public float m_MaxChargeTime = 0.75f; // How long the shell can charge for before it is fired at max force. private string m_FireButton; // The input axis that is used for launching shells. private float m_CurrentLaunchForce; // The force that will be given to the shell when the fire button is released. private float m_ChargeSpeed; // How fast the launch force increases, based on the max charge time. private bool m_Fired; // Whether or not the shell has been launched with this button press. // RongRong : 绑定数值必须在放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 绑定预制体 m_PlayerNumber = GetInt("m_PlayerNumber", 1); m_Shell = GetComponent<Rigidbody>("m_Shell"); m_MinLaunchForce = GetFloat("m_MinLaunchForce", 15f); m_AimSlider = GetComponent<Slider>("m_AimSlider"); m_MaxLaunchForce = GetFloat("m_MaxLaunchForce", 30f); m_MaxChargeTime = GetFloat("m_MaxChargeTime", 0.75f); m_FireTransform = GetComponent<Transform>("m_FireTransform"); m_ShootingAudio = GetComponent<AudioSource>("m_ShootingAudio"); if (Application.platform == RuntimePlatform.WebGLPlayer) { ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/ShotCharging.wav", AudioType.WAV, (AudioClip audioClip) => { m_ChargingClip = audioClip; }); ResourceManager.LoadAudioClipAsync("https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles/Tank/WebGL/ShotFiring.wav", AudioType.WAV, (AudioClip audioClip) => { m_FireClip = audioClip; }); } else { m_ChargingClip = GetAudioClip("m_ChargingClip"); m_FireClip = GetAudioClip("m_FireClip"); } } private void OnEnable() { // When the tank is turned on, reset the launch force and the UI m_CurrentLaunchForce = m_MinLaunchForce; m_AimSlider.value = m_MinLaunchForce; } private void Start () { // The fire axis is based on the player number. m_FireButton = "Fire" + m_PlayerNumber; // The rate that the launch force charges up is the range of possible forces by the max charge time. m_ChargeSpeed = (m_MaxLaunchForce - m_MinLaunchForce) / m_MaxChargeTime; } private void Update () { // The slider should have a default value of the minimum launch force. m_AimSlider.value = m_MinLaunchForce; // If the max force has been exceeded and the shell hasn't yet been launched... if (m_CurrentLaunchForce >= m_MaxLaunchForce && !m_Fired) { // ... use the max force and launch the shell. m_CurrentLaunchForce = m_MaxLaunchForce; Fire (); } // Otherwise, if the fire button has just started being pressed... else if (Input.GetButtonDown (m_FireButton)) { // ... reset the fired flag and reset the launch force. m_Fired = false; m_CurrentLaunchForce = m_MinLaunchForce; // Change the clip to the charging clip and start it playing. m_ShootingAudio.clip = m_ChargingClip; m_ShootingAudio.Play (); } // Otherwise, if the fire button is being held and the shell hasn't been launched yet... else if (Input.GetButton (m_FireButton) && !m_Fired) { // Increment the launch force and update the slider. m_CurrentLaunchForce += m_ChargeSpeed * Time.deltaTime; m_AimSlider.value = m_CurrentLaunchForce; } // Otherwise, if the fire button is released and the shell hasn't been launched yet... else if (Input.GetButtonUp (m_FireButton) && !m_Fired) { // ... launch the shell. Fire (); } } private void Fire () { // Set the fired flag so only Fire is only called once. m_Fired = true; // Create an instance of the shell and store a reference to it's rigidbody. // RongRong : 'Instantiate' 改为 'GameObject.Instantiate' Rigidbody shellInstance = GameObject.Instantiate (m_Shell, m_FireTransform.position, m_FireTransform.rotation) as Rigidbody; // Set the shell's velocity to the launch force in the fire position's forward direction. shellInstance.velocity = m_CurrentLaunchForce * m_FireTransform.forward; // Change the clip to the firing clip and play it. m_ShootingAudio.clip = m_FireClip; m_ShootingAudio.Play (); // Reset the launch force. This is a precaution in case of missing button events. m_CurrentLaunchForce = m_MinLaunchForce; } } }
- 修改'Assets/_Completed-Assets/HotUpdateScripts/Managers/UIDirectionControl.cs',以下是完整版和免费版对应的改后热更新代码(两者相同):
using UnityEngine; namespace CSharpLike// RongRong : 命名空间改为 "CSharpLike" 或者前面使用 "using CSharpLike;". { /// <summary> /// RongRong : 本类包含函数 'Start/Update', /// 我们使用 'HotUpdateBehaviourUpdate' 来绑定预制体, 设置 'scriptUpdateFPS' 数值为 10000. /// 预制体设置 'Booleans' : "m_UseRelativeRotation". /// </summary> public class UIDirectionControl : LikeBehaviour // RongRong : 基类 'MonoBehaviour' 改为 'LikeBehaviour' { // This class is used to make sure world space UI // elements such as the health bar face the correct direction. public bool m_UseRelativeRotation = true; // Use relative rotation should be used for this gameobject? private Quaternion m_RelativeRotation; // The local rotatation at the start of the scene. // RongRong : 绑定数值必须在放在Awake函数内,因为执行顺序为 : Awake -> OnEnable -> Start. void Awake() { // RongRong : 绑定预制体的数值 m_UseRelativeRotation = GetBoolean("m_UseRelativeRotation", true); } private void Start () { m_RelativeRotation = transform.parent.localRotation; } private void Update () { if (m_UseRelativeRotation) transform.rotation = m_RelativeRotation; } } }
-
修改预制体和场景:
- 复制目录'\Assets\_Completed-Assets\Prefabs' 到 '\Assets\_Completed-Assets\HotUpdatePrefabs'. 即我们将要把原来目录'\Assets\_Completed-Assets\Prefabs'里的预制体改成目录'\Assets\_Completed-Assets\HotUpdatePrefabs'里的预制体
- 最终改好后的预制体详见GitHub里的TankFree项目,下面简述一些差异:
- 依次双击打开目录内的预制体,如果里面有绑定'\Assets\_Completed-Assets\Scripts'目录内的非热更新代码, 请在相同位置添加对应的HotUpdateBehaviourXXXXX组件,然后按需设置绑定参数
- 删除被替换成热更新代码了的原组件.这步建议最后才移除, 因为可能预制体或场景之间存在互相引用的,暂时不移除可以方便查看引用
- 注意预制体之间的引用尽量都在'\Assets\_Completed-Assets\HotUpdatePrefabs'内的预制体. 即目录'\Assets\_Completed-Assets\Prefabs'被删掉也不应该受影响
- 里面的AudioClip组件的引用都设为None,因为我们这里都通过'ResourceManager.LoadAudioClipAsync'下载绑定了
- 然后我们逐一地把目录'\Assets\_Completed-Assets\HotUpdateScripts'里的预制体进行修改:
- 我们复制'Assets/_Completed-Assets/Prefabs'整个目录为'Assets/_Completed-Assets/HotUpdatePrefabs'目录
- 编辑器内双击打开场景'Assets/_Complete-Game.unity',菜单'File'->'Save As...'另存为'Assets/_Complete-Game_HotUpdate.unity'
- 修改场景'Assets/_Complete-Game_HotUpdate.unity'.
- 修改GameManager节点:移除原GameManager组件,新增HotUpdateBehaviour组件 (C#Like免费版的是HotUpdateBehaviourUpdate组件,'Script Update FPS'设置为9999), (WebGL平台的,把'Audio Source'组件的AudioClip设置为None)
- 修改CameraRig节点:移除原CameraControl组件,新增HotUpdateBehaviourFixedUpdate组件
- WebGL平台下,修改'CameraRig/Main Camera'节点:去掉'Post-process Layer'组件的勾. (WebGL下,不知为何勾上后就是各种报错,所以禁用该组件)
- 修改GameManager节点:移除原GameManager组件,新增HotUpdateBehaviour组件 (C#Like免费版的是HotUpdateBehaviourUpdate组件,'Script Update FPS'设置为9999), (WebGL平台的,把'Audio Source'组件的AudioClip设置为None)
- 修改预制体'Assets/_Completed-Assets/HotUpdatePrefabs/CompleteShell.prefab': 移除原ShellExplosion组件,新增HotUpdateBehaviourTrigger组件. (WebGL平台的,修改'CompleteShell/CompleteShellExplosion'节点里的'Audio Source'组件的AudioClip设置为None)
- 修改预制体'Assets/_Completed-Assets/HotUpdatePrefabs/CompleteTank.prefab'
- 移除原TankHealth组件,新增HotUpdateBehaviour组件,如下图设置:
- 移除原TankShooting组件,新增HotUpdateBehaviourUpdate组件,如下图设置:
- 移除原TankMovement组件,新增HotUpdateBehaviourCommon组件,如下图设置:
- 'CompleteTank/Canvas/HealthSlider'节点,移除原UIDirectionControl组件,新增HotUpdateBehaviourUpdate组件,如下图设置:
- (WebGL平台的,'Audio Source'组件的AudioClip设置为None)
- 移除原TankHealth组件,新增HotUpdateBehaviour组件,如下图设置:
- 修改预制体'Assets/_Completed-Assets/HotUpdatePrefabs/CompleteTankExplosion.prefab', (WebGL平台的,'Audio Source'组件的AudioClip设置为None)
- 设置AssetBundle名字和后缀,编辑器的Project面板选中'Assets/_Complete-Game_HotUpdate.unity', 在Inspector面板的'Asset Labels'设置AssetBundle:名字为'tanks',后缀为'ab',如下图设置:
- 这个时候直接点编辑器里点'▶'运行,应该可以正常调试运行的
-
配置裁剪'Assets/C#Like/link.xml'
-C#Like(Free)导出脚本的时候会自动识别用到代码部分用到的UnityEngine模块生成到'link.xml';
-C#Like(Free)导出AssetBundle的时候会自动识别部分用到的UnityEngine模块生成到'link.xml';
-没有识别到的部分,需要你自行添加到'Assets/C#Like/Editor/customUsingModule.txt',如下- UnityEngine.InputLegacyModule
- Unity.Postprocessing.Runtime
- com.unity.multiplayer-hlapi.Runtime
-
修改C#Like配置: 菜单'Window'->'C#Like'->打开'C#Like Setting'面板
最终的配置如图:
- 'Hot Update Script Folder'设置为'Assets/_Completed-Assets/HotUpdateScripts' (可以拖放目录)
- 'Product Name'设置为'Tank'
- 'Display Name'设置为'Tanks! Tutorial'
- 'Default Scene'设置为'Assets/_Complete-Game_HotUpdate.unity' (可以拖放文件)
- 'Download Path'设置为'https://www.csharplike.com/CSharpLikeFreeDemo/AssetBundles'
- 'New JSON Key'可以设置自定义JSON字符串. 例如键'Icon'对应值'ABC'
-
编辑器测试调试
- 如需Visual Studio断点调试代码, 菜单"Window"->"C#Like"->"C#Like Setting"->勾上"is debug mode" 来开启调试模式.
- C#Like完整版因为比免费支持更多C#特性,更加接近调试模式. 即免费版会出现更多调试模式下没问题但非调试模式下有问题的情况.
- 在编辑器里点'▶'按钮就可以正常运行调试可热更新的场景了.
- 为了方便测试,我们加了一个"Exit"按钮,用于退出当前游戏返回最初始的场景.
-
导出最终产品AssetBundle
- 菜单"Window"->"C#Like"->"C#Like Setting"->点击按钮"Rebuild Scripts"
- 最终生成目录"TankFree\AssetBundles\Tank",同时也会复制到目录"StreamingAssets"
- "TankFree\AssetBundles\Tank\WebGL\code.ab" : 是包含二进制代码文件的AssetBundle
- "TankFree\AssetBundles\Tank\WebGL\tanks.ab" : 是包含资源(场景)的AssetBundle
- "TankFree\AssetBundles\Tank\WebGL\config.json" : 是ResourceManager用到的配置JSON文件,作用类似manifest文件
本系列文章导读:
- Unity热更新方案C#Like(一)-序言
- Unity热更新方案C#Like(二)-导出官方示范的例子,确认方案可行性
- Unity热更新方案C#Like(三)-详解支持的C#特性:类
- Unity热更新方案C#Like(四)-详解支持的C#特性:委托和Lambda
- Unity热更新方案C#Like(五)-详解支持的C#特性:运算表达式
- Unity热更新方案C#Like(六)-详解支持的C#特性:循环语法
- Unity热更新方案C#Like(七)-详解支持的C#特性:get/set访问器
- Unity热更新方案C#Like(八)-详解支持的C#特性:多线程
- Unity热更新方案C#Like(九)-详解支持的C#特性:Using和命名空间
- Unity热更新方案C#Like(十)-详解支持的C#特性:宏和区域
- Unity热更新方案C#Like(十一)-详解支持的C#特性:枚举
- Unity热更新方案C#Like(十二-详解支持的C#特性:参数修饰符
- Unity热更新方案C#Like(十三)-详解支持的C#特性:函数重载和默认参数
- Unity热更新方案C#Like(十四)-详解支持的C#特性:异常处理
- Unity热更新方案C#Like(十五)-详解支持的C#特性:关键字:unsafe typeof nameof $ @ #pragma #warning #error
- Unity热更新方案C#Like(十六)-详解支持的C#特性:其他杂项:初始值设定项,表达式主体,内联变量声明
- Unity热更新方案C#Like(十七)-详解支持的长链接Socket和WebSocket
- Unity热更新方案C#Like(十八)-详解如何和Unity交互
- Unity热更新方案C#Like(十九)-详解KissJSON:唯一可以在本热更新框架使用的JSON库
- Unity热更新方案C#Like(二十)-详解KissCSV:一个简易实用的CSV表格读取方式
- Unity热更新方案C#Like(廿一)-详解KissFrameworkServer:对应的示范例子和官网所用的服务器框架
- Unity热更新方案C#Like(廿二)-详解内置的例子C#Like Demo:飞机大战,简易聊天室,简易账号/物品/邮件系统
- Unity热更新方案C#Like(廿三)-实战:示范如何把Unity官方免费例子Tanks! Tutorial转成可热更新项目
- Unity热更新方案C#Like(廿四)-实战:示范如何把Unity官方免费例子Platformer Microgame转成可热更新项目
- Unity热更新方案C#Like(廿五)-实战:示范如何建立初始包CSharpLikeFreeDemo项目
- Unity热更新方案C#Like(廿六)-(可选)详解免费版的演示如何升级到完整版的演示
这篇关于Unity热更新方案C#Like(廿三)-实战:示范如何把Unity官方免费例子Tanks! Tutorial转成可热更新项目的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!