unity3D实现人机坦克对战

目录

前言

人机坦克对战建模——感知、思考、行为模型

感知

思考

行为

资源包导入

Kawaii Tank模型导入

标准资源包Assets

游戏场景布置

控制脚本实现

相机的选择控制

相机的缩放控制

坦克的碰撞器属性控制

炮管的后坐力效果

游戏对象破碎效果的实现

坦克受损和销毁的逻辑

用于控制开火的行为

生成子弹和火焰的效果

游戏控制器

目标的移动控制

坦克车轮的控制

坦克车轮旋转的控制

游戏效果展示

开始游戏

攻击敌方

击中敌方后血量显示

剩余血量

被击败(被摧毁)

结语


前言

在学习了一个学期的unity3D游戏设计课程后,已初步掌握了unity3D游戏设计的设计流程、具体游戏实现以及相关的游戏建模。在学习的过程中,我们已经掌握了MVC模型以及带有Action的模型,它们都是进行游戏设计与实现过程中常见的游戏设计模型。其中,利用该模型,我也完成了相应的游戏实例,比如魔鬼与牧师游戏的MVC版本以及动作分离版本,还有打飞碟和射击游戏的实现。在这一个过程中,加深了我对游戏模型的认识。

而在3D游戏设计课程的结课大作业中,我将会尝试着使用一个新的游戏设计模型——感知、思考、行为模型,来编写一个人机坦克对战的小游戏。

我们首先来看一些游戏的视频效果吧:人机坦克对战演示视频

游戏的仓库地址为:github仓库地址

人机坦克对战建模——感知、思考、行为模型

感知

在人机坦克大战游戏中,AI坦克通过视觉感知来获取导航信息。然而,由于存在障碍物的阻挡,这限制了AI坦克的行动,使其不会盲目地朝向玩家涌去。这种限制增加了游戏的挑战和可玩性。此外,AI坦克无法实时瞄准玩家坦克,从而要求玩家坦克在游戏中采取更加策略性的行动。

当AI坦克感知到游戏世界时,它会依靠视觉感知来获取导航信息。通过扫描视野范围内的环境,AI坦克能够识别出地形、障碍物以及玩家的位置。然而,这并不意味着AI坦克能够直接冲向玩家。相反,由于有障碍物的存在,AI坦克需要进行路径规划和导航,以避免碰撞或受阻。

AI坦克会分析感知到的导航信息,并根据预先定义的策略和算法来做出决策。它会考虑到障碍物的位置和形状,以及玩家的相对位置。基于这些信息,AI坦克会选择最佳的路径来接近玩家,同时尽量避开障碍物。

此外,AI坦克并不能实时瞄准玩家。这意味着它不能准确地跟踪玩家的位置和移动,增加了游戏的不确定性和挑战性。玩家需要巧妙地运用战术和策略,以躲避AI坦克的攻击并寻找机会反击。

通过这种感知过程,AI坦克在游戏中表现出更加智能和有策略的行为。玩家需要面对AI坦克的导航限制和瞄准能力的不足,从而提高游戏的可玩性和挑战性。

思考

AI坦克会分析感知到的导航信息,包括地形、障碍物和玩家的位置。它会评估障碍物对路径的影响,考虑到避免碰撞和受阻的因素。AI坦克会计算最佳路径以接近玩家,同时尽可能避开障碍物。它可能会使用路径规划算法(如A*算法)来找到最短和最安全的路径。而在这里主要使用了Unity自带的寻路组件"Navigation"进行“思考”寻路。

行为

AI坦克会根据思考的结果决定移动方向和速度,以沿着最佳路径前进。当遇到障碍物时,它会调整移动方向,选择绕过障碍物的路径。AI坦克的行动可能会受到策略和算法的约束,以避免陷入困境或采取不理想的行为。如果玩家位置发生变化,AI坦克可能会重新评估导航信息并调整行为。

在游戏中,每个AI坦克都具备发射子弹的能力。然而,为了保持游戏的平衡和可玩性,AI坦克不能连续不断地发射子弹,否则游戏将失去挑战。因此,为了限制子弹的发射频率,每个AI坦克会在一段时间后准备好下一次射击。

同时,游戏中的地形具有凹凸不平的特点,这会影响子弹的轨迹。当子弹被发射出去时,它可能会受到地形的影响而产生一定的偏差。这意味着玩家和AI坦克需要在考虑地形因素的基础上,调整瞄准和防御策略。

因此,在游戏中,AI坦克通过控制子弹发射的间隔时间来限制其射击频率,以保持游戏的平衡和玩法的多样性。同时,地形的凹凸不平性也增加了战斗的复杂性,使得子弹的轨迹不完全可控,需要玩家和AI坦克在战斗中做出更加精准的判断和决策。这样的设计考虑了游戏的挑战性和可玩性,使得玩家能够享受到策略性和战术性的对战体验。

资源包导入

Kawaii Tank模型导入

在本次游戏的设计实现中,我们主要利用了unity资源商店的kawaii Tank模型。

因此,我们首先需要在我们的游戏项目中导入这一个资源包。我们首先需要在资源商店中搜索kawaii Tank,然后选取这一个Free的资源,然后点击添加至我的资源中,随后选择在unity中打开,并且选择download将资源下载,随后点击导入即可。

标准资源包Assets

随后,我们需要导入Unity的标准Assets,我们首先在unity官方网站中下载standard Assets,然后通过Assets—import Package—Custom Package将标准资源包Assets导入。

游戏场景布置

首先选择我们导入的资源商店的模型kawaii Tank的Kawaii_Tanks_Project中的Scenes中的Test_Field,我们需要将其另存为场景Tank_battle,如图所示。

随后我们打开新的场景Tank_battle,我们需要对原有的场景布局进行修改,增加地面凹凸不平物以及房子、树、篱笆等遮挡物。同时一共需要三个AI坦克作为攻击方,将其初始位置置于玩家坦克的对面,拉开距离。

然后我们需要添加Navigation窗口,通过Window—AI—Navigation(Obsolete)添加Navigation窗口,然后打开Bake界面,点击按钮Bake生成导航网格图。

控制脚本实现

相机的选择控制

1. Awake(): 当脚本实例被加载时调用的函数。它初始化变量,例如thisTransform是用于存储组件信息的Transform类型变量,angY和angZ是绕Y轴和Z轴的旋转角度,targetAng_Y是绕Y轴的目标旋转角度。

2. Update(): 每帧调用的函数。根据玩家是否在控制状态来更新变量。

3. Mobile_Input(): 处理移动平台上相机的输入。通过检测按钮按下、按住和松开的状态,获取手指的位置信息,计算相机的旋转角度,并将相机的旋转应用到thisTransform.rotation上。

4. Desktop_Input(): 处理桌面平台上相机的输入。调用Mouse_Input_Drag()函数来处理鼠标的拖拽输入。

5. Mouse_Input_Drag(bool isFreeAiming): 处理鼠标的拖拽输入。根据鼠标的移动量计算相机的旋转角度,并将相机的旋转应用到thisTransform.rotation上。

6. Get_ID_Script(ID_Control_CS tempScript): 获取ID_Control_CS脚本的引用。

7. Pause(bool isPaused): 用于暂停相机的旋转效果。

using UnityEngine;
using System.Collections;

#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif

//实现了相机的旋转控制
// This script must be attached to "Camera_Pivot".
namespace ChobiAssets.KTP
{
	
	public class Camera_Rotate_CS : MonoBehaviour
	{

		//用于存储组件信息
		Transform thisTransform;
		//存储鼠标位置
		Vector2 previousMousePos;
		float angY;   //绕Y轴的旋转角度
		float angZ;   //绕Z轴的旋转角度
		float targetAng_Y ;   //绕Y轴的目标旋转角度

		#if UNITY_ANDROID || UNITY_IPHONE
		bool isButtonDown = false;    //标记是否按下了相机控制的按钮
		int fingerID;                 //用于存储手指的ID
		#endif

		ID_Control_CS idScript;       //存储脚本的引用

		void Awake ()
		{
			//变量绑定
			thisTransform = transform;
			angY = thisTransform.eulerAngles.y;
			targetAng_Y = angY;
			angZ = thisTransform.eulerAngles.z;
		}

		void Update ()
		{
			//变量更新
			if (idScript.isPlayer) {
				#if UNITY_ANDROID || UNITY_IPHONE
				Mobile_Input () ;
				#else
				Desktop_Input ();
				#endif
			}
		}

		#if UNITY_ANDROID || UNITY_IPHONE
		//处理移动平台的相机输入
		void Mobile_Input ()
		{
			if (CrossPlatformInputManager.GetButtonDown ("Camera_Press")) {
				isButtonDown = true ;
				#if UNITY_EDITOR
				previousMousePos = Input.mousePosition;
				#else
				fingerID = Input.touches.Length - 1;
				previousMousePos = Input.touches [fingerID].position;
				#endif
				return;
			}
			if (isButtonDown && CrossPlatformInputManager.GetButton ("Camera_Press")) {
				#if UNITY_EDITOR
				Vector3 currentMousePos = Input.mousePosition;
				#else
				Vector3 currentMousePos = Input.touches [fingerID].position;
				#endif
				float horizontal = (currentMousePos.x - previousMousePos.x);
				//float vertical = (currentMousePos.y - previousMousePos.y);
				targetAng_Y += horizontal * 0.5f;
				angY = targetAng_Y;
				//angZ -= vertical * 0.2f;
				previousMousePos = currentMousePos ;
			} else if (CrossPlatformInputManager.GetButtonUp ("Camera_Press")) {
				isButtonDown = false;
			}
			angY = Mathf.MoveTowardsAngle (angY, targetAng_Y, 180.0f * Time.deltaTime);
			thisTransform.rotation = Quaternion.Euler (0.0f, angY, angZ);
		}
		#else

		//处理平台上的相机输入
		void Desktop_Input ()
		{
			Mouse_Input_Drag (true);
		}

		//处理鼠标的拖拽输入
		void Mouse_Input_Drag (bool isFreeAiming)
		{
			if (idScript.aimButton == false) {
				if (idScript.dragButtonDown) {
					previousMousePos = Input.mousePosition;
				}
				if (idScript.dragButton) {
					float horizontal = (Input.mousePosition.x - previousMousePos.x) * 0.1f;
					targetAng_Y += horizontal * 3.0f;
					angY = targetAng_Y;
					//float vertical = (Input.mousePosition.y - previousMousePos.y) * 0.1f;
					//angZ -= vertical * 2.0f;
					previousMousePos = Input.mousePosition;
				}
			}
			angY = Mathf.MoveTowardsAngle (angY, targetAng_Y, 180.0f * Time.deltaTime);
			thisTransform.rotation = Quaternion.Euler (0.0f, angY, angZ);
		}
		#endif

		//获取脚本的引用
		void Get_ID_Script (ID_Control_CS tempScript)
		{
			idScript = tempScript;
		}

		//用于暂停相机的旋转效果
		void Pause (bool isPaused)
		{ // Called from "Game_Controller_CS".
			this.enabled = !isPaused;
		}

	}

}

相机的缩放控制

1. Awake(): 在脚本实例被加载时调用的函数。它初始化了一些变量和组件,包括获取摄像机组件和音频监听器组件,并设置初始位置。

2. Update(): 在每一帧更新时调用的函数。它检测玩家输入,并根据输入控制摄像机的缩放。同时调用Cast_Ray()函数来检测摄像机是否需要自动缩放。

3. Cast_Ray(): 发射射线,检测摄像机与场景中的物体之间是否有碰撞。如果有碰撞,根据碰撞距离调整摄像机的缩放位置。如果没有碰撞,恢复摄像机的初始位置。

4. Get_ID_Script(ID_Control_CS tempScript): 从ID_Control_CS脚本中获取对应的组件引用。根据获取的组件引用决定是否启用摄像机和音频监听器。

5. Pause(bool isPaused): 从"Game_Controller_CS"脚本调用。根据传入的参数决定是否暂停摄像机的更新。

6. Switch_Player(bool isPlayer): 根据传入的参数决定是否启用摄像机和音频监听器。

using UnityEngine;
using System.Collections;

#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif

// This script must be attached to "Main Camera".
// (Note.) Main Camera must be placed on X Aixs of "Camera_Pivot".
//用于控制摄像机的缩放
namespace ChobiAssets.KTP
{
	
	public class Camera_Zoom_CS : MonoBehaviour
	{

		Transform thisTransform;
		Transform parentTransform;
		Transform rootTransform;
		Camera thisCamera;
		AudioListener thisAudioListener;
		float posX;
		float targetPosX;
		int layerMask = ~((1 << 10) + (1 << 2)); // Layer 2 = Ignore Ray, Layer 10 = Ignore All.
		float storedPosX;
		bool autoZoomFlag;
		float hitCount;

		#if UNITY_ANDROID || UNITY_IPHONE
		bool isButtonDown = false;
		Vector2 previousMousePos;
		int fingerID;
		#endif

		public float speed = 30.0f;

		ID_Control_CS idScript;

		//初始化一些变量和组件,包括获取摄像机组件和音频监听器组件,设置初始位置
		void Awake ()
		{
			this.tag = "MainCamera";
			thisCamera = GetComponent < Camera > ();
			thisCamera.enabled = false;
			thisAudioListener = GetComponent < AudioListener > ();
			thisAudioListener.enabled = false;
			thisTransform = transform;
			parentTransform = thisTransform.parent;
			rootTransform = thisTransform.root;
			posX = transform.localPosition.x;
			targetPosX = posX;
		}

		//在每一帧更新时调用
        //检测玩家输入并根据输入控制摄像机的缩放
		//调用Cast_Ray()函数检测摄像机是否需要自动缩放
		void Update ()
		{
			if (idScript.isPlayer) {
				#if UNITY_ANDROID || UNITY_IPHONE
				if (CrossPlatformInputManager.GetButtonDown ("Zoom_Press")) {
					isButtonDown = true ;
				#if UNITY_EDITOR
					previousMousePos = Input.mousePosition;
				#else
					fingerID = Input.touches.Length - 1;
					previousMousePos = Input.touches [fingerID].position;
				#endif
					return;
				}
				if (isButtonDown && CrossPlatformInputManager.GetButton ("Zoom_Press")) {
				#if UNITY_EDITOR
					Vector3 currentMousePos = Input.mousePosition;
				#else
					Vector3 currentMousePos = Input.touches [fingerID].position;
				#endif
					float vertical = (currentMousePos.y - previousMousePos.y);
					targetPosX += vertical * 0.1f;
					targetPosX = Mathf.Clamp (targetPosX, 3.0f, 20.0f);
					previousMousePos = currentMousePos ;
				} else if (CrossPlatformInputManager.GetButtonUp ("Zoom_Press")) {
					isButtonDown = false;
				}
				#else
				float axis = Input.GetAxis ("Mouse ScrollWheel");
				if (axis != 0) {
					#if UNITY_WEBGL
					targetPosX -= axis * 10.0f;
					#else
					targetPosX -= axis * 30.0f;
					#endif
					targetPosX = Mathf.Clamp (targetPosX, 3.0f, 20.0f);
				}
				#endif
				if (posX != targetPosX) {
					posX = Mathf.MoveTowards (posX, targetPosX, speed * Time.deltaTime);
					thisTransform.localPosition = new Vector3 (posX, thisTransform.localPosition.y, thisTransform.localPosition.z);
				} else {
					Cast_Ray ();
				}
			}
		}

		//发射射线,检测摄像机与场景中的物体之间是否有碰撞
		//如果有碰撞,根据碰撞距离调整摄像机的缩放位置
		//如果没有碰撞,恢复摄像机的初始位置
		void Cast_Ray () {
			Ray ray = new Ray (parentTransform.position, thisTransform.position - parentTransform.position);
			RaycastHit[] raycastHits;
			raycastHits = Physics.SphereCastAll (ray, 0.5f, thisTransform.localPosition.x + 1.0f, layerMask);
			foreach (RaycastHit raycastHit in raycastHits) {
				if (raycastHit.transform.root != rootTransform) { // not itself.
					hitCount += Time.deltaTime;
					if (hitCount > 0.5f) {
						hitCount = 0.0f;
						if (autoZoomFlag == false) {
							autoZoomFlag = true;
							storedPosX = posX;
							targetPosX = raycastHit.distance;
							targetPosX = Mathf.Clamp (targetPosX, 3.0f, 20.0f);
						} else {
							if (targetPosX > raycastHit.distance) {
								targetPosX = raycastHit.distance;
								targetPosX = Mathf.Clamp (targetPosX, 3.0f, 20.0f);
							}
						}
					}
					return;
				}
			}
			hitCount = 0.0f;
			if (autoZoomFlag) {
				autoZoomFlag = false;
				targetPosX = storedPosX;
			}
		}

		//从ID_Control_CS脚本中获取对应的组件引用
		//根据获取的组件引用决定是否启用摄像机和音频监听器
		void Get_ID_Script (ID_Control_CS tempScript)
		{
			idScript = tempScript;
			if (idScript.isPlayer) {
				thisAudioListener.enabled = true;
				thisCamera.enabled = true;
			}
			idScript.mainCamScript = this;
		}

		//从"Game_Controller_CS"脚本调用
		//根据传入的参数决定是否暂停摄像机的更新
		void Pause (bool isPaused)
		{ // Called from "Game_Controller_CS".
			this.enabled = !isPaused;
		}

		//根据传入的参数决定是否启用摄像机和音频监听器
		public void Switch_Player (bool isPlayer)
		{
			thisAudioListener.enabled = isPlayer;
			thisCamera.enabled = isPlayer;
		}

	}

}

坦克的碰撞器属性控制

1. Awake(): 在脚本实例被加载时调用的函数。它用于设置装甲碰撞器的属性。首先,通过GetComponent<Collider>()获取当前脚本附加的游戏对象上的碰撞器组件,然后将其设置为一个触发器(trigger)。接着,通过GetComponent<MeshRenderer>()获取当前脚本附加的游戏对象上的网格渲染器组件,并将其设置为在游戏中不可见,即禁用渲染器。

2. damageMultiplier: 装甲的伤害倍数。这个变量用于设置装甲的伤害倍数,可以根据需要进行调整。

using UnityEngine;
using System.Collections;

//用于管理装甲的碰撞器的属性
namespace ChobiAssets.KTP
{
	
	public class Armor_Collider_CS : MonoBehaviour
	{
		[ Header ("Armor settings")]
		//用于设置装甲的伤害倍数
		[ Tooltip ("Multiplier for the damage.")] public float damageMultiplier = 1.0f;

		void Awake ()
		{
			// Make it a trigger and invisible.
			//获取当前脚本附加的游戏对象上的碰撞器组件,然后设置其为一个触发器
			GetComponent < Collider > ().isTrigger = true;
			//获取当前脚本附加的游戏对象上的网格渲染器组件,并将其设置为在游戏中不可见
			GetComponent < MeshRenderer > ().enabled = false;
		}

	}

}

炮管的后坐力效果

1. Awake(): 在脚本实例被加载时调用的函数。它用于绑定和初始化组件。通过this.transform获取脚本所附加的游戏对象的Transform组件,然后将其赋值给thisTransform变量。同时,将炮管的初始本地位置存储在initialPos变量中。

2. IEnumerator Recoil_Brake(): 这是一个协程方法,用于控制炮管的后坐力效果。协程是一种特殊的函数,可以在一段时间内暂停执行,并在稍后继续执行。在这个协程中,首先通过正弦函数计算炮管的位置偏移量,并将其应用到炮管的本地坐标上,实现后坐力的前冲效果。然后,再通过正弦函数计算炮管的位置偏移量,将其应用到炮管的本地坐标上,实现炮管回位的效果。最后,表示后坐力效果已经完成。

3. recoilTime: 后坐力前冲的持续时间,以秒为单位。可以根据需要进行调整。

4. returnTime: 炮管回位的持续时间,以秒为单位。可以根据需要进行调整。

5. length: 炮管移动的长度,以米为单位。可以根据需要进行调整。

6. thisTransform: Transform组件的引用,用于存储脚本所附加的游戏对象的Transform组件。

7. isReady: 表示坐后力效果是否准备就绪的布尔值。当炮管坐后力效果正在进行时,设置为false,以防止重复触发后坐力。

8. initialPos: 用于存储炮管的初始本地位置的Vector3变量。

9. Fire(): 用于触发后坐力效果的公共方法。当Fire()方法被调用时,首先检查坐后力效果是否准备就绪。如果准备就绪,将isReady设置为false,然后启动协程Recoil_Brake()来实现后坐力效果。

using UnityEngine;
using System.Collections;

// This script must be attached to "Barrel_Base".
//用于控制炮管的坐后力效果
namespace ChobiAssets.KTP
{
	
	public class Barrel_Control_CS : MonoBehaviour
	{
		//设置了浮点型变量,用于设置了坐后力制动器地参数
		[ Header ("Recoil Brake settings")]
		[ Tooltip ("Time it takes to push back the barrel. (Sec)")] public float recoilTime = 0.05f;    //炮管坐后力时间
		[ Tooltip ("Time it takes to to return the barrel. (Sec)")] public float returnTime = 0.05f;    //炮管回位的持续时间
		[ Tooltip ("Movable length for the recoil brake. (Meter)")] public float length = 0.3f;         //炮管移动的长度

		Transform thisTransform;           //组件变量
		bool isReady = true;               //表示坐后力效果是否准备就绪
		Vector3 initialPos;                //用于存储炮管的初始本地位置
		const float HALF_PI = Mathf.PI * 0.5f;   //用于后续的计算

		void Awake ()
		{
			//绑定和初始化组件
			thisTransform = this.transform;
			initialPos = thisTransform.localPosition;
		}

		//一个协程方法,用于控制炮管的后坐力效果
		IEnumerator Recoil_Brake ()
		{
			//用于后坐力前冲
			// Move backward.
			//在一定的时间recoilTime内,通过计算当前时间与总时间的比例
			//使用正弦函数计算炮管的位置偏移量,并将其应用到炮管的本地坐标上
			float count = 0.0f;
			while (count < recoilTime) {
				float rate = Mathf.Sin (HALF_PI * (count / recoilTime));
				thisTransform.localPosition = new Vector3 (initialPos.x, initialPos.y, initialPos.z - (rate * length));
				count += Time.deltaTime;
				yield return null;
			}
			// Return to the initial position.
			//用于炮管回位
			//在一定的时间returnTime内,通过计算当前时间与总时间的比例
			//使用正弦函数计算炮管的位置偏移量,并将其应用到炮管的本地坐标上
			count = 0.0f;
			while (count < returnTime) {
				float rate = Mathf.Sin (HALF_PI * (count / returnTime) + HALF_PI);
				thisTransform.localPosition = new Vector3 (initialPos.x, initialPos.y, initialPos.z - (rate * length));
				count += Time.deltaTime;
				yield return null;
			}
			//表示后坐力效果已经完成
			isReady = true;
		}

		//用于触发后坐力效果
		public void Fire ()
		{ // Called from "Fire_Control_CS".
			//首先检查是否已经准备就绪
			if (isReady) {
				isReady = false;
				//启动协程
				StartCoroutine ("Recoil_Brake");
			}
		}
	}

}

游戏对象破碎效果的实现

1. Awake(): 在脚本实例被加载时调用的函数。它用于绑定组件对象。通过transform获取脚本所附加的游戏对象的Transform组件,然后将其赋值给thisTransform变量。

2. OnJointBreak(): 当与当前对象连接的关节断开时调用的方法。该方法使用协程Broken()来触发对象的破碎效果。

3. OnTriggerEnter(Collider collider): 当与当前对象发生碰撞的触发器对象进入时调用的方法。如果进入的碰撞体不是一个触发器(isTrigger属性为false),则使用协程Broken()来触发对象的破碎效果。

4. brokenPrefab: 破碎对象的预制体。这是一个公共变量,可以在Inspector面板中指定破碎对象的预制体。

5. lagTime: 破碎对象的延迟时间,以秒为单位。在破碎效果触发之前,等待一段时间。

6. thisTransform: Transform组件的引用,用于存储脚本所附加的游戏对象的Transform组件。

7. IEnumerator Broken(): 这是一个协程方法,用于实现对象的破碎效果。在协程中,首先等待一段时间(由lagTime指定),然后在当前对象的位置和旋转处实例化破碎对象的预制体。最后,销毁当前对象。

using UnityEngine;
using System.Collections;

//用于实现游戏中游戏对象破碎的效果
namespace ChobiAssets.KTP
{
	
	public class Break_Object_CS : MonoBehaviour
	{
		//用于设置游戏对象破碎的参数
		[ Header ("Broken object settings")]
		[ Tooltip ("Prefab of the broken object.")] public GameObject brokenPrefab;    //破碎对象的预制体
		[ Tooltip ("Lag time for breaking. (Sec)")] public float lagTime = 1.0f;       //破碎对象的延迟时间

		Transform thisTransform;     //组件对象

		void Awake ()
		{
			thisTransform = transform;   //绑定组件对象
		}

		void OnJointBreak ()
		{
			//它使用协程来调用名为Broken的方法
			//在游戏中,当与当前对象连接的关节断开时,该方法会触发对象的破碎效果
			StartCoroutine ("Broken");
		}

		void OnTriggerEnter (Collider collider)
		{
			if (collider.isTrigger == false) {
				StartCoroutine ("Broken");
			}
		}

		//用于实现对象的破碎效果
		IEnumerator Broken ()
		{
			//首先等待一段时间lagTime
			yield return new WaitForSeconds (lagTime);
			if (brokenPrefab) {
				//然后在当前对象的位置和旋转处实例化破碎对象的预制体
				Instantiate (brokenPrefab, thisTransform.position, thisTransform.rotation);
			}
			//销毁当前对象
			Destroy (gameObject);
		}

	}

}

坦克受损和销毁的逻辑

1. Start(): 在脚本启动时调用的函数。它用于记录初始耐久度(initialDurability)。

2. Set_DamageText(): 根据设置的参数实例化伤害文本(Damage Text)的预制体,并将其设置到Canvas中进行显示。它获取Damage_Display_CS脚本的引用,用于更新伤害文本的显示。根据设置的Canvas名称(canvasName),找到对应的Canvas对象,并将伤害文本设置为其子对象。如果找不到对应的Canvas对象,则打印警告信息。

3. Update(): 在每一帧更新时调用的函数。如果是玩家控制的坦克,根据玩家的输入决定是否启动销毁逻辑。如果是玩家控制的坦克,调用displayScript.Get_Damage()更新伤害文本的显示。

4. Get_Damage(float damageValue): 从子弹脚本(Bullet_Nav_CS)调用的函数。根据传入的伤害值(damageValue)减少坦克的耐久度(durability)。如果坦克的耐久度仍然大于零,则调用displayScript.Get_Damage()更新伤害文本的显示。如果坦克的耐久度小于等于零,则开始销毁坦克。

5. Start_Destroying(): 开始销毁坦克的逻辑。向坦克的所有部件发送销毁消息("Destroy")。如果设置了destroyedPrefab,则在坦克位置实例化destroyedPrefab。移除伤害文本(displayScript)。销毁当前脚本。

6. Get_ID_Script(ID_Control_CS tempScript): 从ID_Control_CS脚本中获取对应的组件引用。根据获取的组件引用设置坦克的伤害文本。

7. Pause(bool isPaused): 从"Game_Controller_CS"脚本调用的函数。根据传入的参数决定是否暂停当前脚本的更新。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif


//用于控制坦克的受伤和销毁的逻辑
namespace ChobiAssets.KTP
{
	
	public class Damage_Control_CS : MonoBehaviour
	{
		[Header ("Damage settings")]
		[Tooltip ("Durability of this tank.")] public float durability = 300.0f;
		[Tooltip ("Prefab used for destroyed effects.")] public GameObject destroyedPrefab;
		[Tooltip ("Prefab of Damage Text.")] public GameObject textPrefab;
		[Tooltip ("Name of the Canvas used for Damage Text.")] public string canvasName = "Canvas_Texts";

		Transform bodyTransform;
		Damage_Display_CS displayScript;
		float initialDurability;

		ID_Control_CS idScript;

		//在启动脚本时调用
		//记录初始耐久度(initialDurability)
		void Start ()
		{ // Do not change to "Awake()".
			initialDurability = durability;
		}

		//根据设置的参数实例化伤害文本(Damage Text)的预制体,并将其设置到Canvas中进行显示
		//获取Damage_Display_CS脚本的引用,用于更新伤害文本的显示
		//根据设置的Canvas名称(canvasName)找到对应的Canvas对象,并将伤害文本设置为其子对象
		//如果找不到对应的Canvas对象,则打印警告信息
		void Set_DamageText ()
		{
			if (textPrefab == null || string.IsNullOrEmpty (canvasName) || durability == Mathf.Infinity) {
				return;
			}
			// Instantiate Damage Text, and set it to the Canvas.
			GameObject textObject = Instantiate (textPrefab, Vector3.zero, Quaternion.identity) as GameObject;
			displayScript = textObject.GetComponent <Damage_Display_CS> ();
			displayScript.targetTransform = bodyTransform;
			GameObject canvasObject = GameObject.Find (canvasName);
			if (canvasObject) {
				displayScript.transform.SetParent (canvasObject.transform);
				displayScript.transform.localScale = Vector3.one;
			} else {
				Debug.LogWarning ("Canvas for Damage Text cannot be found.");
			}
		}

		//在每一帧更新时调用
		//如果是玩家控制的坦克,根据玩家的输入决定是否启动销毁逻辑
		//如果是玩家控制的坦克,调用displayScript.Get_Damage()更新伤害文本的显示
		void Update ()
		{
            // 游戏玩家一直显示血条,AI坦克受到攻击后显示血条
            if(idScript.isPlayer)
            {
                displayScript.Get_Damage(durability, initialDurability);
            }
            // Destruct
            if (idScript.isPlayer) {
				#if UNITY_ANDROID || UNITY_IPHONE
				if (CrossPlatformInputManager.GetButtonDown ("Destruct")) {
				#else
				if (Input.GetKeyDown (KeyCode.Return)) {
				#endif
					Start_Destroying ();
				}
			}
		}

		//从子弹脚本(Bullet_Nav_CS)调用
		//根据传入的伤害值(damageValue)减少坦克的耐久度(durability)
		//如果坦克的耐久度仍然大于零,则调用displayScript.Get_Damage()更新伤害文本的显示
		//如果坦克的耐久度小于等于零,则开始销毁坦克
		public void Get_Damage (float damageValue)
		{ // Called from "Bullet_Nav_CS".
			durability -= damageValue;
			if (durability > 0.0f) { // Still alive.
				// Display Text
				if (displayScript) {
					displayScript.Get_Damage (durability, initialDurability);
				}
			} else { // Dead
				Start_Destroying ();
			}
		}

		//开始销毁坦克的逻辑
		//向坦克的所有部件发送销毁消息("Destroy")
		//如果设置了destroyedPrefab,则在坦克位置实例化destroyedPrefab
		//移除伤害文本(displayScript)
		//销毁当前脚本
		void Start_Destroying ()
		{
            if(idScript.isPlayer == false)
            {
                this.gameObject.SetActive(false);
            }
			// Send message to all the parts.
			BroadcastMessage ("Destroy", SendMessageOptions.DontRequireReceiver);
			// Create destroyedPrefab.
			if (destroyedPrefab) {
				GameObject tempObject = Instantiate (destroyedPrefab, bodyTransform.position, Quaternion.identity) as GameObject;
				tempObject.transform.parent = bodyTransform;
			}
			// Remove the Damage text.
			if (displayScript) {
				Destroy (displayScript.gameObject);
			}
			// Destroy this script.
			Destroy (this);
		}

		//从ID_Control_CS脚本中获取对应的组件引用
        //根据获取的组件引用设置坦克的伤害文本
		void Get_ID_Script (ID_Control_CS tempScript)
		{
			idScript = tempScript;
			bodyTransform = idScript.storedTankProp.bodyTransform;
			Set_DamageText ();
		}

		//从"Game_Controller_CS"脚本调用
        //根据传入的参数决定是否暂停当前脚本的更新
		void Pause (bool isPaused)
		{ // Called from "Game_Controller_CS".
			this.enabled = !isPaused;
		}

	}

}

用于控制开火的行为

1. Awake(): 在脚本启动时调用的函数。它获取当前游戏对象的Transform组件的引用,并获取该游戏对象下的所有Barrel_Control_CS和Fire_Spawn_CS脚本的引用。

2. Update(): 在每一帧更新时调用的函数。如果该游戏对象是玩家操控的坦克(由idScript.isPlayer属性判断),则根据平台类型调用Mobile_Input()或Desktop_Input()来接收输入。如果该游戏对象不是玩家操控的坦克,则根据一定时间间隔调用Fire()来进行自动开火。

3. Mobile_Input(): 在移动平台上接收输入的函数。如果按下了名为"GunCam_Press"的按钮,并且当前可以开火(isReady为true),则调用Fire()来进行开火。

4. Desktop_Input(): 在桌面平台上接收输入的函数。如果设置了fireButton,并且当前可以开火(isReady为true),则调用Fire()来进行开火。

5. Fire(): 开火函数。调用barrelScripts和fireScripts的Fire()方法来执行开火操作。如果该游戏对象是玩家操控的坦克(由idScript.isPlayer属性判断),则执行后坐力效果,并进入重新装填状态。启动Reload协程来控制重新装填时间。

6. Reload(): 重新装填协程。在一定时间(reloadTime)后将isReady设置为true,表示可以进行下一次开火。

7. Destroy(): 被Damage_Control_CS调用的函数。销毁当前脚本组件。

8. Get_ID_Script(ID_Control_CS tempScript): 由ID_Control_CS调用的函数。获取ID_Control_CS脚本的引用,并缓存其属性值。获取关联的刚体组件的引用。

9. Pause(bool isPaused): 由Game_Controller_CS调用的函数。根据传入的isPaused参数,启用或禁用当前脚本组件,实现暂停或恢复行为控制。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif

// This script must be attached to "Cannon_Base".
//用于控制开火行为
namespace ChobiAssets.KTP
{
	public class Fire_Control_CS : MonoBehaviour
	{

		[Header ("Fire control settings")]
		[Tooltip ("Loading time. (Sec)")] public float reloadTime = 0.1f;
		[Tooltip ("Recoil force with firing.")] public float recoilForce = 100.0f;

		bool isReady = true;
		Transform thisTransform;
		Rigidbody bodyRigidbody;

		ID_Control_CS idScript;
		Barrel_Control_CS[] barrelScripts;
		Fire_Spawn_CS[] fireScripts;

        private float count = 0;


		//在脚本启动时调用
		//获取当前游戏对象的Transform组件
		//获取当前游戏对象下的所有Barrel_Control_CS和Fire_Spawn_CS脚本的引用
		void Awake ()
		{
			thisTransform = this.transform;
			barrelScripts = GetComponentsInChildren <Barrel_Control_CS> ();
			fireScripts = GetComponentsInChildren <Fire_Spawn_CS> ();
		}

		//在每一帧更新时调用
		//如果该游戏对象是玩家操控的坦克(由idScript.isPlayer属性判断),则根据平台类型调用Mobile_Input()或Desktop_Input()来接收输入
		//如果该游戏对象不是玩家操控的坦克,则根据一定时间间隔调用Fire()来进行自动开火
		void Update ()
		{
			if (idScript.isPlayer) {
				#if UNITY_ANDROID || UNITY_IPHONE
				Mobile_Input ();
				#else
				Desktop_Input ();
				#endif
			} else
            {
                count = count + Time.deltaTime;
                if(count > 3.0f)
                {
                    Fire();
                    count = 0;
                }
            }
		}

		//用于在移动平台上接收输入
		//如果按下了名为"GunCam_Press"的按钮,并且当前可以开火(isReady为true),则调用Fire()来进行开火
		#if UNITY_ANDROID || UNITY_IPHONE
		void Mobile_Input ()
		{
			if (CrossPlatformInputManager.GetButtonUp ("GunCam_Press") && isReady) {
				Fire ();
			}
		}
		#else

		//用于在桌面平台上接收输入
		//如果设置了fireButton,并且当前可以开火(isReady为true),则调用Fire()来进行开火
		void Desktop_Input ()
		{
			if (idScript.fireButton && isReady) {
				Fire ();
			}
		}
		#endif

		//开火函数
		//调用barrelScripts和fireScripts的Fire()方法来执行开火操作
		//如果该游戏对象是玩家操控的坦克(由idScript.isPlayer属性判断),则执行后坐力效果,并进入重新装填状态
		//启动Reload协程来控制重新装填时间
		void Fire ()
		{
			// Call barrelScripts and fireScripts to fire.
			for (int i = 0; i < barrelScripts.Length; i++) {
				barrelScripts [i].Fire ();
			}
			for (int i = 0; i < fireScripts.Length; i++) {
				fireScripts [i].StartCoroutine ("Fire");
			}
            if(idScript.isPlayer)
            {
                // Add recoil shock.
                bodyRigidbody.AddForceAtPosition(-thisTransform.forward * recoilForce, thisTransform.position, ForceMode.Impulse);
                isReady = false;
                StartCoroutine("Reload");
            }
			
		}

		//重新装填协程
		//在一定时间(reloadTime)后将isReady设置为true,表示可以进行下一次开火
		IEnumerator Reload ()
		{
			yield return new WaitForSeconds (reloadTime);
			isReady = true;
		}

		//被"Damage_Control_CS"调用
		//销毁当前脚本组件
		void Destroy ()
		{ // Called from "Damage_Control_CS".
			Destroy (this);
		}

		//由"ID_Control_CS"调用
		//获取ID_Control_CS脚本的引用,并缓存其属性值
		//获取关联的刚体组件的引用
		void Get_ID_Script (ID_Control_CS tempScript)
		{
			idScript = tempScript;
			bodyRigidbody = idScript.storedTankProp.bodyRigidbody;
		}

		//由"Game_Controller_CS"调用
		//根据传入的isPaused参数,启用或禁用当前脚本组件,实现暂停或恢复行为控制
		void Pause (bool isPaused)
		{ // Called from "Game_Controller_CS".
			this.enabled = !isPaused;
		}

	}

}

生成子弹和火焰的效果

1. Awake(): 在脚本启动时调用的函数。它获取当前游戏对象的Transform组件的引用。

2. firePrefab: 火焰效果的预制体,用于生成火焰特效。

3. bulletPrefab: 子弹的预制体,用于生成子弹。

4. attackForce: 子弹的攻击力。

5. bulletVelocity: 子弹的速度,以米每秒为单位。

6. spawnOffset: 生成子弹时的偏移距离,以米为单位。

7. thisTransform: 当前游戏对象的Transform组件的引用。

8. Fire(): 开火协程。在协程中,首先实例化火焰效果(如果有定义firePrefab),并将其放置在当前游戏对象的位置和旋转下。然后实例化子弹(如果有定义bulletPrefab)并将其放置在当前游戏对象的前方一定偏移距离的位置。接着获取子弹对象上的Bullet_Nav_CS脚本引用,并设置其attackForce属性。计算子弹的初始速度tempVelocity,以当前游戏对象的前方方向和设定的子弹速度(bulletVelocity)为基础。在下一个FixedUpdate周期中,将子弹刚体的速度设置为tempVelocity,实现子弹的发射。

using UnityEngine;
using System.Collections;

// This script must be attached to "Fire_Point".
//用于生成子弹和火焰效果
namespace ChobiAssets.KTP
{
	
	public class Fire_Spawn_CS : MonoBehaviour
	{

		[ Header ("Firing settings")]
		[ Tooltip ("Prefab of muzzle fire.")] public GameObject firePrefab;
		[ Tooltip ("Prefab of bullet.")] public GameObject bulletPrefab;
		[ Tooltip ("Attack force of the bullet.")] public float attackForce = 100.0f;
		[ Tooltip ("Speed of the bullet. (Meter per Second)")] public float bulletVelocity = 250.0f;
		[ Tooltip ("Offset distance for spawning the bullet. (Meter)")] public float spawnOffset = 1.0f;

		Transform thisTransform;

		//在脚本启动时调用
		//获取当前游戏对象的Transform组件
		void Awake ()
		{
			thisTransform = this.transform;
		}

		//开火协程
		//实例化火焰效果(如果有firePrefab)并放置在当前游戏对象的位置和旋转下
		//实例化子弹(如果有bulletPrefab)并放置在当前游戏对象前方一定偏移距离的位置
		//获取子弹对象上的Bullet_Nav_CS脚本引用,并设置attackForce属性
		//计算子弹的初始速度tempVelocity,以当前游戏对象的前方方向和设定的子弹速度(bulletVelocity)为基础
		//在下一个FixedUpdate周期中设置子弹刚体的速度为tempVelocity,实现子弹的发射
		public IEnumerator Fire ()
		{
			// Muzzle Fire
			if (firePrefab) {
				GameObject fireObject = Instantiate (firePrefab, thisTransform.position, thisTransform.rotation) as GameObject;
				fireObject.transform.parent = thisTransform;
			}
			// Shot Bullet
			if (bulletPrefab) {
				GameObject bulletObject = Instantiate (bulletPrefab, thisTransform.position + thisTransform.forward * spawnOffset, thisTransform.rotation) as GameObject;
				bulletObject.GetComponent <Bullet_Nav_CS> ().attackForce = attackForce;
				Vector3 tempVelocity = thisTransform.forward * bulletVelocity;
				// Shoot
				yield return new WaitForFixedUpdate ();
				bulletObject.GetComponent <Rigidbody> ().velocity = tempVelocity;
			}
		}

	}

}

游戏控制器

1. fixedTimestep: 物理计算的固定时间步长。

2. maxResolution: 移动设备的最大分辨率。

3. touchControlsPrefab: 触摸控制器的预制体。

4. tankList: 坦克列表,存储所有的坦克。

5. currentID: 当前可操作的坦克的ID。

6. isPaused: 游戏是否暂停。

7. Awake(): 在脚本启动时调用的函数。设置物理计算的固定时间步长(fixedTimestep)。初始化坦克列表(tankList)。如果在移动设备上,实例化触摸控制器(touchControlsPrefab)。根据移动设备的最大分辨率(maxResolution)调整屏幕分辨率。设置游戏对象的标签为"GameController"。设置层之间的碰撞关系,包括重置和忽略碰撞。

8. Receive_ID(ID_Control_CS idScript): 接收ID_Control_CS脚本发送的ID。存储坦克的组件信息(bodyTransform和bodyRigidbody)。将坦克添加到坦克列表(tankList)中。如果ID为0,表示当前操作的是玩家坦克。

9. Update(): 在每一帧更新中调用。切换当前可操作的坦克。如果按下Tab键(或者在移动设备上按下Switch按钮),将当前ID加1,并确保不超过坦克列表的长度。处理退出游戏的逻辑。处理重新加载场景的逻辑。处理暂停游戏的逻辑。

10. Pause(): 暂停游戏。切换isPaused的值。根据isPaused设置时间缩放(timeScale)。向所有坦克发送暂停消息。

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

#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif

//用于整体的游戏控制
namespace ChobiAssets.KTP
{
	[ System.Serializable]
	public class TankProp
	{
		public Transform bodyTransform;
		public Rigidbody bodyRigidbody;
	}

	public class Game_Controller_CS : MonoBehaviour {

		[Header ("Game settings")]
		[Tooltip ("Interval for physics calculations.")] public float fixedTimestep = 0.03f;
		#if UNITY_ANDROID || UNITY_IPHONE
		[Tooltip ("Maximum resolution for mobile device.")] public int maxResolution = 720;
		#endif

		[Header ("Stage settings")]
		[Tooltip ("Set the prefab of 'Touch_Controls'.")] public GameObject touchControlsPrefab;

		List <ID_Control_CS> tankList;
		int currentID = 0;
		bool isPaused;

		//在脚本启动时调用
		//设置物理计算的固定时间步长(fixedTimestep)
		//初始化坦克列表(tankList)
		//如果是在移动设备上(UNITY_ANDROID || UNITY_IPHONE),实例化触摸控制器(touchControlsPrefab)
		//根据移动设备的最大分辨率(maxResolution)调整屏幕分辨率
		//设置游戏对象的标签为"GameController"
		//设置层之间的碰撞关系,包括重置和忽略碰撞
		void Awake ()
		{
			Time.fixedDeltaTime = fixedTimestep;
			tankList = new List <ID_Control_CS> ();
			#if UNITY_ANDROID || UNITY_IPHONE
			if (touchControlsPrefab) {
				Instantiate (touchControlsPrefab);
			}
			float screenRate = (float)maxResolution / Screen.height;
			if (screenRate > 1.0f) {
				screenRate = 1.0f;
			}
			int width = (int)(Screen.width * screenRate);
			int height = (int)(Screen.height * screenRate);
			Screen.SetResolution(width, height, true);
			#endif
			this.tag = "GameController";
			/*
			Layer Collision Settings.
			Layer9 >> for wheels.
			Layer10 >> for Suspensions.
			Layer11 >> for MainBody.
			*/
			for (int i = 0; i <= 11; i++) {
				Physics.IgnoreLayerCollision (9, i, false); // Reset settings.
				Physics.IgnoreLayerCollision (11, i, false); // Reset settings.
			}
			Physics.IgnoreLayerCollision (9, 9, true); // Wheels do not collide with each other.
			Physics.IgnoreLayerCollision (9, 11, true); // Wheels do not collide with MainBody.
			for (int i = 0; i <= 11; i++) {
				Physics.IgnoreLayerCollision (10, i, true); // Suspensions do not collide with anything.
			}
		}

		//接收ID_Control_CS脚本发送的ID
		//存储坦克的组件信息(bodyTransform和bodyRigidbody)
		//将坦克添加到坦克列表(tankList)中
		//如果ID为0,表示当前操作的是玩家坦克
		public void Receive_ID (ID_Control_CS idScript)
		{
			// Store the components in the lists.
			TankProp tankProp = new TankProp ();
			tankProp.bodyRigidbody = idScript.GetComponentInChildren <Rigidbody> ();
			tankProp.bodyTransform = tankProp.bodyRigidbody.transform;
			idScript.storedTankProp = tankProp;
			// Add the tank to tankList.
			tankList.Add (idScript);
			// Send currentID (0).
			if (idScript.id == 0) {
				idScript.isPlayer = true;
			} else {
				idScript.isPlayer = false;
			}
		}

		//在每一帧更新中调用
		//切换当前可操作的坦克
		//如果按下Tab键(或者在移动设备上按下Switch按钮),将当前ID加1,并确保不超过坦克列表的长度
		//处理退出游戏的逻辑
		//处理重新加载场景的逻辑
		//处理暂停游戏的逻辑
		void Update ()
		{
			// Switch operable tank.
			#if UNITY_ANDROID || UNITY_IPHONE
			if (CrossPlatformInputManager.GetButtonDown ("Switch")) {
			#else
			if (Input.GetKeyDown (KeyCode.Tab)) {
			#endif
				currentID += 1;
				if (currentID > tankList.Count - 1) {
					currentID = 0;
				}
				for (int i = 0; i < tankList.Count; i++) {
					tankList [i].Get_Current_ID (currentID);
				}
			}
			// Quit
			if (Input.GetKeyDown (KeyCode.Escape)) {
				Application.Quit ();
			}
			// Reload the scene.
			#if UNITY_ANDROID || UNITY_IPHONE
			if (CrossPlatformInputManager.GetButtonDown ("Restart")) {
			#else
			if (Input.GetKeyDown (KeyCode.Backspace)) {
			#endif
				if (isPaused) {
					Pause ();
				}
				SceneManager.LoadScene (SceneManager.GetActiveScene ().name);
			}
			// Pause.
			#if UNITY_ANDROID || UNITY_IPHONE
			if (CrossPlatformInputManager.GetButtonDown ("Pause")) {
			#else
			if (Input.GetKeyDown ("p")) {
			#endif
				Pause ();
			}
		}

		//暂停游戏
		//切换isPaused的值
		//根据isPaused设置时间缩放(timeScale)
		//向所有坦克发送暂停消息
		void Pause ()
		{
			isPaused = !isPaused;
			// Set timeScale and text.
			if (isPaused) {
				Time.timeScale = 0.0f;
			} else {
				Time.timeScale = 1.0f;
			}
			// Send Message to all the tanks.
			ID_Control_CS[] idScripts = FindObjectsOfType <ID_Control_CS> ();
			foreach (ID_Control_CS idScript in idScripts) {
				idScript.BroadcastMessage ("Pause", isPaused, SendMessageOptions.DontRequireReceiver);
			}
		}

	}

}

目标的移动控制

1. target: 用于获取目标点的游戏对象,在Inspector面板中进行赋值。

2. mr: NavMesh代理组件的引用。

3. Start(): 在脚本启动时调用的函数。获取自身的NavMeshAgent组件,并将其赋值给变量mr。

4. Update(): 每帧调用一次的函数。使用NavMeshAgent的SetDestination()方法将目标点的位置传递给NavMeshAgent组件,以使游戏对象移动到目标点。在Update()函数中,通过mr.SetDestination(target.transform.position)将目标点的位置传递给NavMeshAgent组件,使游戏对象以最优路径移动到目标点。这样,当目标点发生变化时,游戏对象会自动调整路径并移动到新的目标点。

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

//用于将游戏对象移动到目标点
public class PlaceTarget : MonoBehaviour
{
    //public GameObject target:用于获取目标点的游戏对象,在Inspector面板中进行赋值
    //NavMeshAgent mr:NavMesh代理组件的引用
    public GameObject target;  //获取目标点,注意在面板中赋值
    NavMeshAgent mr;   //声明变量
    
    // Use this for initialization
    //在脚本启动时调用
    //获取自身的NavMeshAgent组件,并将其赋值给mr变量
    void Start()
    {
        //获取到自身的NavMeshAgent组件
        mr = GetComponent<NavMeshAgent>();
    }

    // Update is called once per frame
    //每帧调用一次
    //使用NavMeshAgent的SetDestination()方法将目标点的位置传递给NavMeshAgent组件,以使游戏对象移动到目标点
    void Update()
    {
        //使用属性将目标点的坐标进行传递
        //mr.destination = target.transform.position;
        //使用方法获取目标点坐标,,和前一行代码作用相同
        mr.SetDestination(target.transform.position);
    }
}

坦克车轮的控制

1. Awake(): 在脚本被加载时调用的函数。在该函数中,设置了游戏对象的层级,获取了Rigidbody组件,并设置了刚体的迭代次数。

2. Update(): 在每一帧更新时调用的函数。根据不同平台(Android、iPhone或桌面)的输入方式,调用相应的输入处理函数。

3. Mobile_Input(): 用于处理移动平台上的输入(Android或iPhone)。根据玩家的操作,更新车辆的行驶方向。

4. Desktop_Input(): 用于处理桌面平台上的输入。根据玩家的操作,更新车辆的行驶方向。

5. FixedUpdate(): 在固定的时间间隔内调用的函数。用于更新车辆的物理属性,例如速度、角速度和停车制动。

6. Destroy(): 在游戏对象被销毁时调用的函数。禁用刚体的约束,并在一段时间后销毁游戏对象。

7. Disable_Constraints(): 协程方法,用于在一段时间后禁用刚体的约束,并最终销毁脚本所附加的游戏对象。

8. Get_ID_Script(): 用于获取与脚本交互的ID_Control_CS脚本的引用。

9. Pause(): 用于根据游戏是否暂停来启用或禁用脚本的更新。

using UnityEngine;
using System.Collections;

#if UNITY_ANDROID || UNITY_IPHONE
using UnityStandardAssets.CrossPlatformInput;
#endif

//用于车轮控制
// This script must be attached to "MainBody".
namespace ChobiAssets.KTP
{
	
	public class Wheel_Control_CS : MonoBehaviour
	{
		[ Header ("Driving settings")]
		[ Tooltip ("Torque added to each wheel.")] public float wheelTorque = 3000.0f; // Reference to "Wheel_Rotate".
		[ Tooltip ("Maximum Speed (Meter per Second)")] public float maxSpeed = 7.0f; // Reference to "Wheel_Rotate".
		[ Tooltip ("Rate for ease of turning."), Range (0.0f, 2.0f)] public float turnClamp = 0.8f;
		[ Tooltip ("'Solver Iteration Count' of all the rigidbodies in this tank.")] public int solverIterationCount = 7;

		// Reference to "Wheel_Rotate".
		[HideInInspector] public float leftRate;
		[HideInInspector] public float rightRate;

		Rigidbody thisRigidbody;
		bool isParkingBrake = false;
		float lagCount;
		float speedStep;
		float autoParkingBrakeVelocity = 0.5f;
		float autoParkingBrakeLag = 0.5f;

		ID_Control_CS idScript;

		/* for reducing Calls.
		Wheel_Rotate_CS[] rotateScripts;
		*/

		//这是 MonoBehaviour 类的一个生命周期方法,在游戏对象被加载时调用
		//Awake() 方法用于执行一些初始化操作
		//它设置了游戏对象的层级、获取 Rigidbody 组件,并设置刚体的迭代次数
		void Awake ()
		{
			this.gameObject.layer = 11; // Layer11 >> for MainBody.
			thisRigidbody = GetComponent < Rigidbody > ();
			thisRigidbody.solverIterations = solverIterationCount;
			/* for reducing Calls.
			rotateScripts = GetComponentsInChildren <Wheel_Rotate_CS> ();
			*/
		}

		//在每一帧更新时调用,Update() 方法用于根据输入更新车辆的行驶方向
		void Update ()
		{
			if (idScript.isPlayer) {
				#if UNITY_ANDROID || UNITY_IPHONE
				Mobile_Input ();
				#else
				Desktop_Input ();
				#endif
			}
		}

		//用于处理移动平台(Android 或 iPhone)上的输入。根据玩家的操作,它更新车辆的行驶方向
		#if UNITY_ANDROID || UNITY_IPHONE
		void Mobile_Input ()
		{
			if (CrossPlatformInputManager.GetButtonDown ("Up")) {
				speedStep += 0.5f;
				speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
			} else if (CrossPlatformInputManager.GetButtonDown ("Down")) {
				speedStep -= 0.5f;
				speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
			}
			float vertical = speedStep;
			float horizontal = 0.0f;
			if (CrossPlatformInputManager.GetButton ("Left")) {
				horizontal = Mathf.Lerp (-turnClamp, -1.0f, Mathf.Abs (vertical / 1.0f));
			} else if (CrossPlatformInputManager.GetButton ("Right")) {
				horizontal = Mathf.Lerp (turnClamp, 1.0f, Mathf.Abs (vertical / 1.0f));
			}
			if (vertical < 0.0f) {
				horizontal = -horizontal; // like a brake-turn.
			}
			leftRate = Mathf.Clamp (-vertical - horizontal, -1.0f, 1.0f);
			rightRate = Mathf.Clamp (vertical - horizontal, -1.0f, 1.0f);
		}
		#else

		//用于处理桌面平台上的输入。根据玩家的操作,它更新车辆的行驶方向
		void Desktop_Input ()
		{
			if (Input.GetKeyDown (KeyCode.UpArrow) || Input.GetKeyDown (KeyCode.W)) {
				speedStep += 0.5f;
				speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
			} else if (Input.GetKeyDown (KeyCode.DownArrow) || Input.GetKeyDown (KeyCode.S)) {
				speedStep -= 0.5f;
				speedStep = Mathf.Clamp (speedStep, -1.0f, 1.0f);
			} else if (Input.GetKeyDown (KeyCode.X)) {
				speedStep = 0.0f;
			}
			float vertical = speedStep;
			float horizontal = Input.GetAxis ("Horizontal");
			float clamp = Mathf.Lerp (turnClamp, 1.0f, Mathf.Abs (vertical / 1.0f));
			horizontal = Mathf.Clamp (horizontal, -clamp, clamp);
			if (vertical < 0.0f) {
				horizontal = -horizontal; // like a brake-turn.
			}
			leftRate = Mathf.Clamp (-vertical - horizontal, -1.0f, 1.0f);
			rightRate = Mathf.Clamp (vertical - horizontal, -1.0f, 1.0f);
		}
		#endif

		//在固定的时间间隔内调用,FixedUpdate() 方法用于更新车辆的物理属性,例如速度、角速度和停车制动
		void FixedUpdate ()
		{
			// Auto Parking Brake using 'RigidbodyConstraints'.
			if (leftRate == 0.0f && rightRate == 0.0f) {
				float velocityMag = thisRigidbody.velocity.magnitude;
				float angularVelocityMag = thisRigidbody.angularVelocity.magnitude;
				if (isParkingBrake == false) {
					if (velocityMag < autoParkingBrakeVelocity && angularVelocityMag < autoParkingBrakeVelocity) {
						lagCount += Time.fixedDeltaTime;
						if (lagCount > autoParkingBrakeLag) {
							isParkingBrake = true;
							thisRigidbody.constraints = RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ | RigidbodyConstraints.FreezeRotationY;
						}
					}
				} else {
					if (velocityMag > autoParkingBrakeVelocity || angularVelocityMag > autoParkingBrakeVelocity) {
						isParkingBrake = false;
						thisRigidbody.constraints = RigidbodyConstraints.None;
						lagCount = 0.0f;
					}
				}
			} else {
				isParkingBrake = false;
				thisRigidbody.constraints = RigidbodyConstraints.None;
				lagCount = 0.0f;
			}
			/* for reducing Calls.
			for (int i = 0; i < rotateScripts.Length; i++) {
				rotateScripts [i].FixedUpdate_Me ();
			}
			*/
		}

		//在游戏对象被销毁时调用,Destroy() 方法用于处理游戏对象被破坏时的逻辑
		//它会禁用刚体的约束,并在一段时间后销毁游戏对象
		void Destroy ()
		{ // Called from "Damage_Control_CS".
			StartCoroutine ("Disable_Constraints");
		}

		//这个协程方法用于在一段时间后禁用刚体的约束,并最终销毁脚本所附加的游戏对象
		IEnumerator Disable_Constraints ()
		{
			// Disable constraints of MainBody's rigidbody.
			yield return new WaitForFixedUpdate (); // This wait is required for PhysX.
			thisRigidbody.constraints = RigidbodyConstraints.None;
			Destroy (this);
		}

		//这个函数用于获取与脚本交互的 ID_Control_CS 脚本的引用
		void Get_ID_Script (ID_Control_CS tempScript)
		{
			idScript = tempScript;
		}

		//这个函数用于根据游戏是否暂停来启用或禁用脚本的更新
		void Pause (bool isPaused)
		{ // Called from "Game_Controller_CS".
			this.enabled = !isPaused;
		}

	}

}

坦克车轮旋转的控制

1. Awake(): 在脚本被加载时调用的函数。在该函数中,设置了车轮的层级为第9层,获取了车轮的刚体组件,判断车轮是否在左侧,获取车轮的初始旋转角度,查找并获取父物体的Wheel_Control_CS脚本,根据车轮的半径计算最大角速度。

2. FixedUpdate(): 在固定的时间间隔内调用的函数。根据车轮是左侧还是右侧,获取对应的转速比率。根据转速比率和控制脚本中的轮胎扭矩,给车轮施加扭矩。设置车轮的最大角速度,根据转速比率和之前计算的最大角速度。通过稳定角度来保持车轮的角度。

3. Destroy(): 在游戏对象被销毁时调用的函数。设置车轮的角阻尼为无穷大,并销毁脚本自身。

4. Pause(): 当游戏暂停时调用的函数,根据传入的布尔值参数来启用或禁用脚本的更新。

using UnityEngine;
using System.Collections;

// This script must be attached to all the Driving Wheels.
//用于控制车轮的旋转
namespace ChobiAssets.KTP
{
	
	public class Wheel_Rotate_CS : MonoBehaviour
	{

		bool isLeft;                          //用于表示车轮是否位于左侧
		Rigidbody thisRigidbody;              //用于存储车轮的刚体组件
		float maxAngVelocity;                 //最大角速度,根据车辆的最大速度和车轮半径计算得出
		Wheel_Control_CS controlScript;       //用于存储 Wheel_Control_CS 脚本的引用,该脚本控制车辆的行驶 
		Transform thisTransform;              //当前车轮的 Transform 组件
		Transform parentTransform;            //当前车轮的父物体的 Transform 组件
		Vector3 angles;                       //存储车轮的初始旋转角度


		//将车轮的层级设置为第9层,用于车轮
		//获取车轮的刚体组件
		//根据车轮在局部坐标系的位置判断车轮是否在左侧
		//获取车轮的初始旋转角度
		//查找并获取父物体的 Wheel_Control_CS 脚本
		//根据车轮的半径计算最大角速度
		void Awake ()
		{
			this.gameObject.layer = 9; // Layer9 >> for wheels.
			thisRigidbody = GetComponent <Rigidbody> ();
			// Set direction.
			if (transform.localPosition.y > 0.0f) {
				isLeft = true;
			} else {
				isLeft = false;
			}
			// Get initial rotation.
			thisTransform = transform;
			parentTransform = thisTransform.parent;
			angles = thisTransform.localEulerAngles;
			// Find controlScript.
			controlScript = parentTransform.parent.GetComponent <Wheel_Control_CS> ();
			// Set maxAngVelocity.
			float radius = GetComponent <SphereCollider> ().radius;
			maxAngVelocity = Mathf.Deg2Rad * ((controlScript.maxSpeed / (2.0f * Mathf.PI * radius)) * 360.0f);
		}

		/* for reducing Calls.
		public void FixedUpdate_Me ()
		*/
		//根据车轮是左侧还是右侧,获取对应的转速比率
		//根据转速比率和控制脚本中的轮胎扭矩,给车轮施加扭矩
		//设置车轮的最大角速度,根据转速比率和之前计算的最大角速度
		//通过稳定角度来保持车轮的角度
		void FixedUpdate ()
		{
			float rate;
			if (isLeft) {
				rate = controlScript.leftRate;
			} else {
				rate = controlScript.rightRate;
			}

			thisRigidbody.AddRelativeTorque (0.0f, Mathf.Sign (rate) * controlScript.wheelTorque, 0.0f);
			thisRigidbody.maxAngularVelocity = Mathf.Abs (maxAngVelocity * rate);
			// Stabilize angle.
			angles.y = thisTransform.localEulerAngles.y;
			thisRigidbody.rotation = parentTransform.rotation * Quaternion.Euler (angles);
		}

		//当游戏对象被破坏时调用,设置车轮的角阻尼为无穷大,并销毁脚本自身
		void Destroy ()
		{ // Called from "Damage_Control_CS".
			thisRigidbody.angularDrag = Mathf.Infinity;
			Destroy (this);
		}

		//当游戏暂停时调用,根据传入的布尔值参数来启用或禁用脚本的更新
		void Pause (bool isPaused)
		{ // Called from "Game_Controller_CS".
			this.enabled = !isPaused;
		}

	}

}

游戏效果展示

开始游戏

攻击敌方

击中敌方后血量显示

剩余血量

被击败(被摧毁)

结语

在这一个人机坦克对战游戏实现中,我们主要运用了感知—思考—行为的模型来对游戏进行建模,同时我们的游戏主要是建立在了kawaii Tank模型上的,我们主要在该模型上进行修改,以及添加各种检测和损坏的效果脚本等。

结束了这一个游戏项目的设计后,本学期的unity3D游戏设计就需要告一段落啦,在这一个学期里,我学习到了很多东西,学会了使用unity这一个主流的游戏设计软件来进行设计和编写游戏。主要学习了各种不同的模型来设计游戏、还学会了粒子效果、UI设计等技术知识,这一段时间的unity3D游戏设计的学习给我带来了极大的兴趣,相信不久的以后还会有使用到unity3D来设计游戏的时候。

特别感谢老师一整个学期的教学,一步一步地指导我们对unity3D进行设计和学习,同时还教会我们自己创建了不同的属于自己地游戏设计,在这个过程中,我们体会到了游戏设计的快乐,以及编程游戏的爱好。虽然这一个过程是艰苦的,但是好在真正地有体验到了游戏设计的编程之美,以及游戏设计的不简单。

我觉得本课程我应该得到优秀,因为我在本学期的学习过程中,善于游戏程序设计技术,熟练掌握了多种设计模式,比如MVC设计模式、带有action的MVC模式、还有感知—思考—行为的模式,充分利用相关的模式与技术来编写代码。同时,我还善于利用不同的UI设计技术来设计游戏以及展示游戏,在本学期的游戏内容中,我不仅可以编写游戏代码,还可以为游戏设计相对精彩的游戏内容以及闯关内容,真正达到锻炼自己的目的,在这个过程中,学习到了很多东西,使得自己的技术不断得到提升。

同时,我还非常擅长写技术普及博客,我写的每一篇博客均获得了比较大的浏览量,很多博客都获得了超过一千的阅读量,同时还获的不少的赞和收藏数。

总而言之,我觉得我本课程应该获得优秀,因为我不仅付出了比别人多的努力和时间,而且对老师布置的每一个作业都认真完成和积极改进,收获了比较大的成就感和真正的硬实力的提升。

  • 28
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值