Unity3D游戏开发案例学习——Tanks!(基本完结)

【2020.1.12】

概述

本案例来自unity官方中级游戏教程Tanks(单机双人坦克大战)
项目同时用于Unity机器学习内容的学习,预定计划为训练可规避障碍物,欲图消灭玩家的坦克ai

第一部分:场景设置

Lighting setting中的部分调整

1.关闭实时烘焙
2.一些未找到的设置:关闭【Backed GI】,使用【Precomputed Realtime GI】并调整实时分辨率由2至0.5
3.调整环境照明为纯色

摄像机相关设置

1.调整摄像机坐标到适宜位置,并旋转到合适的角度
2.投影模式设置为正投影
3.设置背景色,使得背景暴露在镜头中时其视觉效果符合游戏色调

第二部分:坦克的创建和控制

模型导入

对Tank设置相应的Layer属性

刚体组件

为Tank添加Rigbody组件
基本设置不变,冻结Tank的y轴位移和x、z轴旋转

碰撞器组件

为Tank添加Box Collider组件
将碰撞体中心和大小调整到适宜数值

声音资源组件

为Tank添加第一个Audio Source组件(EngineIdel)
选择声音片段EngineIdel,并设置播放属性Loop
为Tank添加第二个Audio Source组件(空置)
此声音资源组件用于Tank开火音效,在这个部分不进行具体实现

添加特效

将DustTrail预制体设置为Tank的子物体并复制,分别命名为RightDustTrail和LeftDustTrail,分别作为Tank的左右轨道移动痕迹特效

将坦克保存为预制体

将构造好的Tank保存为Prefabs

脚本

TankMovement

此脚本用于控制坦克的移动,其具体功能为:
1.获取玩家输入
2.通过代码控制音频、
3.控制坦克前后移动
4.控制坦克水平转向

具体的编程实现如下:

public int m_PlayerNumber = 1;//玩家编号(?)
	public float m_Speed = 12f;//移动速度
	public float m_TurnSpeed = 180f;//转向速度
	public AudioSource m_MovementAudio;//移动音效
	public AudioClip m_EngineIdling;//静止声音片段
	public AudioClip m_EngineDriving;//行驶声音片段
	public float m_PitchRange = 0.2f;//音高变化范围

	private string m_MovementAxisName;//移动控制轴名称(基于PlayerNumber)
	private string m_TurnAxisName;//转向控制轴名称(基于PlayerNumber)
	private Rigidbody m_Rigidbody;//刚体组件
	private float m_MovementInputValue;//移动控制输入
	private float m_TurnInputValue;//转向控制输入
	private float m_OriginalPitch;//原始音高(?)

	private void Awake()
	{
		m_Rigidbody = GetComponent<Rigidbody>();
	}

	private void OnEnable()
	{
		m_Rigidbody.isKinematic = false;//接受动力学模拟
		//重置坦克的输入值,以重新开始输入
		m_MovementInputValue = 0f;
		m_TurnInputValue = 0f;
	}

	private void OnDisable()
	{
		m_Rigidbody.isKinematic = true;
	}

	private void Start()
	{
		m_MovementAxisName = "Vertical" + m_PlayerNumber;
		//对于Player1,其移动输入的轴名称为Vertical1,以此类推
		m_TurnAxisName = "Horizontal" + m_PlayerNumber;

		m_OriginalPitch = m_MovementAudio.pitch;//存储移动音效的原始音高
	}

	private void Update()
	{
		//获取轴输入
		m_MovementInputValue = Input.GetAxis(m_MovementAxisName);
		m_TurnInputValue = Input.GetAxis(m_TurnAxisName);
		//
		EngineAudio();
	}

	private void EngineAudio()//控制坦克音效并调整音高
	{
		if (Mathf.Abs(m_MovementInputValue) < 0.1f && Mathf.Abs(m_TurnInputValue) < 0.1f)//坦克未移动
		{
			if (m_MovementAudio.clip == m_EngineDriving)//当前音效为移动音效
			{
				m_MovementAudio.clip = m_EngineIdling;//将音效切换为静止音效
				m_MovementAudio.pitch = Random.Range(m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange);
				//使音效控制在以原始音高为基础的适宜范围

				m_MovementAudio.Play();//重新播放音效
			}
		}
		else
		{
			if (m_MovementAudio.clip == m_EngineIdling)//当前音效为静止音效
			{
				m_MovementAudio.clip = m_EngineDriving;//将音效切换为移动音效
				m_MovementAudio.pitch = Random.Range(m_OriginalPitch - m_PitchRange, m_OriginalPitch + m_PitchRange);
				//使音效控制在以原始音高为基础的适宜范围

				m_MovementAudio.Play();//重新播放音效
			}
		}
	}

	private void FixedUpdate()
	{
		Move();
		Turn();
	}

	private void Move()//移动方法
	{
		Vector3 movement = transform.forward * m_MovementInputValue * m_Speed * Time.deltaTime;//移动矢量

		m_Rigidbody.MovePosition(m_Rigidbody.position + movement);//使坦克移动到指定的绝对位置
	}

	private void Turn()//转向方法
	{
		float turn = m_TurnInputValue * m_TurnSpeed * Time.deltaTime;//旋转浮点数

		Quaternion turnRotation = Quaternion.Euler(0f, turn, 0f);//?

		m_Rigidbody.MoveRotation(m_Rigidbody.rotation * turnRotation);//?
	}

完成效果图

图2.1项目进度图

存在的问题:

1.坦克旋转控制相关方法(向量、欧拉角、四元数)的有关知识
2.关于代码中变量的命名前缀"m_"
这是C#中的一种变量命名规范,m意为member,表示变量是该类成员变量

第三部分:摄像机控制

创建空物体CameraRig

1.创建空物体CameraRig,用于摄像机的控制,调整其到合适的角度
2.将Camera设置为CameraRig的子物体(此时Camera的位置信息将以CameraRig为中心),调整其坐标到适宜位置

脚本

CameraControl

此脚本用于控制摄像机的移动,注意,脚本附着于CameraRig
此脚本所控制的摄像机能够适应场景中存在多个玩家的情况

具体编程实现如下:

public float m_DampTime = 0.2f;//相机移动凝滞时间
	public float m_ScreenEdgeBuffer = 4f;//?屏幕边缘缓冲
	public float m_MinSize = 6.5f;//最小尺寸
	/*[HideInInspector]*/ public Transform[] m_Targets;//跟随目标

	private Camera m_Camera;//摄像机组件引用
	private float m_ZoomSpeed;//变焦速度
	private Vector3 m_MoveVelocity;//摄像机当前移动速度
	private Vector3 m_DesiredPosition;//期望位置(这里指摄像机在跟随多辆坦克时其中心应位于的平均位置)

	private void Awake()
	{
		m_Camera = GetComponentInChildren<Camera>();
	}

	private void FixedUpdate()
	{
		Move();
		Zoom();
	}

	private void Move()
	{
		FindAveragePosition();

		transform.position = Vector3.SmoothDamp(transform.position, m_DesiredPosition, ref m_MoveVelocity, m_DampTime);
		//平滑阻尼方法
		//参数释义:当前物体位置,目标物体位置,参数按引用传递的当前移动速度(方法每次调用时会修改原本数值),到达目标时间
	}

	private void FindAveragePosition()//获取平均位置
	{
		Vector3 averagePos = new Vector3();//有效目标平均位置向量
		int numTargets = 0;//有效目标数量

		for (int i = 0; i < m_Targets.Length; i++)
		{
			if (!m_Targets[i].gameObject.activeSelf)//目标游戏对象不处于活跃状态(如在一轮中死亡)
				continue;//跳过此轮循环

			averagePos += m_Targets[i].position;//累加位置
			numTargets++;//累加数量
		}

		if (numTargets > 0)
			averagePos /= numTargets;//获得平均位置

		averagePos.y = transform.position.y;//确保摄像机的y轴位置不变,即便坦克在后续修改中有y轴上的移动可能

		m_DesiredPosition = averagePos;
	}

	private void Zoom()//变焦
	{
		float requierdSize = FindRequierdSize();

		m_Camera.orthographicSize = Mathf.SmoothDamp(m_Camera.orthographicSize, requierdSize, ref m_ZoomSpeed, m_DampTime);
		//该语句欲图调整正交摄像机视窗大小
		//参数释义:正交摄像机当前视窗大小,所需的视窗大小,当前变焦速度,完成变焦时间
	}

	private float FindRequierdSize()
	{
		Vector3 desiredLocalPos = transform.InverseTransformPoint(m_DesiredPosition);
		//A.InverseTransformPoint(B)
		//该方法返回B以A为世界坐标原点时的坐标向量,即B相对A的坐标
		//再这里通过此方法获取期望位置相对CameraRig的坐标

		float size = 0f;

		for (int i = 0; i < m_Targets.Length; i++)
		{
			if (!m_Targets[i].gameObject.activeSelf)//目标游戏对象不处于活跃状态(如在一轮中死亡)
				continue;//跳过此轮循环

			Vector3 targetLocalPos = transform.InverseTransformPoint(m_Targets[i].position);
			//获取目标游戏对象相对于CameraRig的坐标
			Vector3 desiredPosToTarget = targetLocalPos - desiredLocalPos;
			//计算期望位置到目标坦克位置的向量
			size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.y));
			//?
			size = Mathf.Max(size, Mathf.Abs(desiredPosToTarget.x) / m_Camera.aspect);
			//?
			//Camera.aspect 屏幕长宽比
		}

		size += m_ScreenEdgeBuffer;

		size = Mathf.Max(size, m_MinSize);//确保调整的目标尺寸不小于目标尺寸

		return size;
	}

	public void SetStartPositionAndSize()//设置起始位置以及尺寸
	{
		FindAveragePosition();//获取平均位置

		transform.position = m_DesiredPosition;//设置CameraRig的坐标到期望位置

		m_Camera.orthographicSize = FindRequierdSize();//获取并设置目标尺寸
	}

完成效果图

图3.1 项目进度图

存在的问题:

1.关于SmoothDamp方法中,第三个参数的意义
2.对于通过FindRequiredSize方法实现正交摄像机调焦的逻辑尚不理解

第四部分:血条设置

Slider组件

1.创建一个新的Slider组件(HealthSlider)
2.调整事件系统中的输入轴避免与坦克控制输入产生冲突
3.缩小Canvas Scaler中的单位像素数值,并修改Canvas设置使画布渲染到World Space中
4.将Canvas设置为Tank的子元素,并适当调整坐标、大小及旋转属性
5.删除Slider组件中的Handle Slide Area,避免玩家手动操纵血条
6.锚定Slider组件的位置
7.关闭Interactable,设置Transition属性为None
8.将Slider的Background的Source Image切换为资源中的Health Wheel,并调整适宜的透明度
9.将Slider的Fill的Source Image同样切换为Health Wheel,调整适宜的透明度
10.将Slider的Fill的Image Type修改为Filled,修改Filled Origin为Left,并取消勾选Clockwise
4.1Slider的有关修改

脚本

UIDirectionControl

此脚本用于锁定坦克生命条的旋转
具体编程实现如下:

public bool m_UseRelativeRotation = true;//?

	private Quaternion m_RelativeRotation;//相对旋转信息

	private void Start()
	{
		m_RelativeRotation = transform.parent.rotation;//使相对旋转跟随Canvas的旋转
	}

	private void Update()
	{
		if (m_UseRelativeRotation)//?
			transform.rotation = m_RelativeRotation;//?
	}

坦克摧毁特效

1.将预制体资源TankExplosion拖入场景
2.为TankExplosion添加Audio Source组件,设置声音片段为TankExplosion(注意取消勾选Play On Awake)
3.完成预制体更新后删除场景中的TankExplosion

脚本

TankHealth

此脚本用于控制坦克的血条变化(注意添加至Tank,并在完成后更新预制体信息)
此脚本作用于坦克本身的生存属性以及血条UI的工作逻辑

具体编程实现如下:

public float m_StartingHealth = 100f;//游戏起始生命值
	public Slider m_Slider;//血条Slider组件引用
	public Image m_FillImage;//填充图像
	public Color m_FullHealthColor = Color.green;//满血颜色
	public Color m_ZeroHealthColor = Color.red;//空血颜色
	public GameObject m_ExplosionPrefabs;//坦克爆炸特效预制体引用

	private AudioSource m_ExplosionAudio;//爆炸音源组件引用
	private ParticleSystem m_ExplosionParticles;//爆炸特效组件引用
	private float m_CurrentHealth;//当前生命值
	private bool m_Dead;//死亡判断布尔变量
	private void Awake()
	{
		m_ExplosionParticles = Instantiate(m_ExplosionPrefabs).GetComponent<ParticleSystem>();
		//生成指定的预制体资源并从中获取组件引用
		m_ExplosionAudio = m_ExplosionParticles.GetComponent<AudioSource>();//获取音源组件引用

		m_ExplosionParticles.gameObject.SetActive(false);//置承载爆炸特效的游戏对象为未激活的状态
	}

	private void OnEnable()
	{
		m_CurrentHealth = m_StartingHealth;//重置当前生命值

		m_Dead = false;

		SetHealthUI();
	}

	public void TakeDamage(float amount)
	{
		m_CurrentHealth -= amount;

		SetHealthUI();

		if (m_CurrentHealth <= 0f && !m_Dead)
		{
			OnDeath();
		}
	}

	private void SetHealthUI()
	{
		m_Slider.value = m_CurrentHealth;//同步血条显示为当前生命值

		m_FillImage.color = Color.Lerp(m_ZeroHealthColor, m_FullHealthColor, m_CurrentHealth / m_StartingHealth);
		//根据当前生命值占总生命值的比例,在空血颜色和满血颜色之间线性插值
	}

	private void OnDeath()
	{
		m_Dead = true;

		m_ExplosionParticles.transform.position = transform.position;//移动爆炸特效到正确位置
		m_ExplosionParticles.gameObject.SetActive(true);//激活爆炸特效承载对象

		m_ExplosionParticles.Play();//启动爆炸特效

		m_ExplosionAudio.Play();//播放爆炸音效

		gameObject.SetActive(false);//消灭坦克(仅SetActive,不进行摧毁)
	}

完成效果图

4.2项目进度图

存在的问题:

1.需要系统学习UGUI的相关知识
2.需要系统学习粒子系统的相关知识

第五部分:子弹

模型导入

导入Model文件夹中的Shell模型

碰撞体(触发器)组件

为Shell添加Capsule Collider,并设置为触发器模式(Is Trigger)(子弹生效不需要其本身与外界发生直接的物理交互)
适当调整触发器的中心相对坐标、半径、高度以及方向

5.1 项目进度图

刚体组件

为Shell添加RigBody组件,基本设置不变

子弹爆炸特效

1.为Shell添加预制体ShellExplosion作为子物体
2.为ShellExplosion添加Audio Source组件以实现子弹爆炸音效(声音片段为ShellExplosion)

光源组件

为Shell添加Light组件

脚本

ShellExplosion

此脚本用于处理子弹爆炸的相关逻辑(注意保存后在Unity中完成组件引用的赋值)
具体编程实现如下:

public LayerMask m_TankMask;//Tank所在层
	public ParticleSystem m_ExplosionParticles;//爆炸粒子系统组件引用
	public AudioSource m_ExplosionAudio;//子弹爆炸音源组件引用
	public float m_MaxDamage = 100f;//最大伤害
	public float m_ExplosionForce = 1000f;//爆炸冲击力
	public float m_MaxLifeTime = 2f;//?最大存在时间
	public float m_ExplosionRadios = 5f;//爆炸半径

	private void Start()
	{
		Destroy(gameObject, m_MaxLifeTime);//若子弹两秒后依然存在则摧毁子弹
	}

	[System.Obsolete]
	private void OnTriggerEnter(Collider other)
	{
		Collider[] colliders = Physics.OverlapSphere(transform.position, m_ExplosionRadios, m_TankMask);
		//返回以参数1为原点和参数2为半径的球体内满足所在Layer为指定Layer的碰撞体集合

		for (int i = 0; i < colliders.Length; i++)//遍历子弹范围内的坦克碰撞体
		{
			Rigidbody TargetRigBody = colliders[i].GetComponent<Rigidbody>();//获取坦克刚体组件引用(?检查是否有刚体)

			if (!TargetRigBody)//?
				continue;

			TargetRigBody.AddExplosionForce(m_ExplosionForce, transform.position, m_ExplosionRadios);
			//对坦克(刚体)添加爆炸力
			//参数1为爆炸力度,参数2为爆炸中心坐标(Vector3),?参数3为爆炸半径

			TankHealth targetHealth = TargetRigBody.GetComponent<TankHealth>();//获取被子弹爆炸击中的坦克的生命值脚本引用
			//通过组件获得组件

			if (!targetHealth)//?
				continue;

			float damage = CalculateDamage(TargetRigBody.position);//计算伤害

			targetHealth.TakeDamage(damage);//造成伤害
		}

		m_ExplosionParticles.transform.parent = null;//将爆炸特效与子弹解耦
		//? 为何仅在Transform层级解耦

		m_ExplosionParticles.Play();//播放爆炸粒子特效

		m_ExplosionAudio.Play();//播放爆炸音效

		Destroy(m_ExplosionParticles, m_ExplosionParticles.duration);//在粒子特效播放结束后摧毁承载对象
		//以上函数中duration已弃用 
		//? 替代方案
		//duration返回粒子特效的持续时间

		Destroy(gameObject);//销毁子弹

		//? 以上事件的发生次序
	}

	private float CalculateDamage(Vector3 TargetPosition)//根据距离计算伤害
	{
		Vector3 explosionToTarget = TargetPosition - transform.position;//爆炸中心到有效目标坦克的距离

		float explosionDistance = explosionToTarget.magnitude;//爆炸距离
		//Vector3.magnitude 返回向量长度数值(float)

		float relativeDistance = (m_ExplosionRadios - explosionDistance) / m_ExplosionRadios;
		/* 获取相对距离(爆炸半径与生效距离的差/爆炸半径),分子即处于爆炸范围内的坦克距离爆炸边界的距离(越大说明距离爆炸中心越近),
		 * 通过比总爆炸半径来获得一个反映所受爆炸强度的小数
		 */
		float damage = relativeDistance * m_MaxDamage;//计算伤害数值(最大伤害乘相对距离)

		damage = Mathf.Max(0f, damage);//确保damage的数值为负数
		//有时在爆炸检测的边缘,坦克的碰撞体被捕获,但其中心坐标在检测球外,以至于得到的relativeDistance为负数,通过如上方式避免这一问题

		return damage;
	}

完成效果图

5.2 项目进度图

存在的问题:

1.爆炸发生后,ExplosionParticle会发生**The variable m_ExplosionParticles of ShellExplosion has not been assigned.**的错误,原因不明
(这里通过Transform有关父类的方法进行解耦,使承载粒子特效的对象和子弹解耦,使得在子弹被销毁时特效仍能正常播放完毕)
2.ParticleSystem.duration(粒子特效持续时间)已被弃用,替代手段?
3.RigBody.AddExplosionForce的作用机理?
4.OnTriggerEnter中一系列方法的进行顺序?

第六部分:发射子弹

开火点

创建空物体FireTransform,设置为坦克的子物体,并适当调整坐标及旋转(Z轴为默认正方向),作为坦克的开火点

指示箭头

此滑块UI用于指示子弹发射的蓄力时长,蓄力时间越长,箭头延伸越长,子弹落点越远

1.在前述的Canvas中创建新的Slider(命名为AimSlider)
2.删除Handle Slide Area,避免滑块和玩家直接交互
3.删除Background,使得箭头仅在进行射击时才被看见
4.调整Slider设置,取消Interactable的勾选,使Slider不可交互
5.调整Slider设置,将Transition属性设置为None
6.调整Slider设置,将Direction属性设置为Bottom To Top(自下而上)
7.Min、Max属性分别设置为15、30
8.通过锚预设调整AimSlider及其子物体的相对位置
9.调整Fill设置,设置适宜的Transform属性,并修改Source Image属性为预制资源Aim Arror
10.调整Aim Slider的边界,使之贴合于Tank两侧,并将其前置到Tank前端,适当抬高高度

脚本

TankShooting

此脚本用于处理子弹发射相关逻辑
具体编程实现如下:

public int m_PlayerNumber = 1;			//玩家数量
	public Rigidbody m_Shell;				//子弹刚体组件引用
	public Transform m_FireTransform;		//开火点变换组件引用
	public Slider m_AimSlider;				//射击指示滑动条组件引用
	public AudioSource m_ShootingAudio;		//射击音源组件引用
	public AudioClip m_ChargingClip;		//充能声音片段
	public AudioClip m_FireClip;			//开火声音片段
	public float m_MinLaunchForce = 15f;	//最小发射力度
	public float m_MaxLaunchForce = 30f;	//最大发射力度
	public float m_MaxChargeTime = 0.75f;   //最大充能时间

	private string m_FireButton;			//射击按键字符串引用
	private float m_CurrentLaunchForce;		//当前发射力度
	private float m_ChargeSpeed;			//? 充能速度
	private bool m_Fired;					//表示是否开火射击的布尔变量

	private void OnEnable()//(其中设置可服务于游戏重开)
	{
		//对起始时当前发射力度变量以及滑动条组件的Value赋初值
		m_CurrentLaunchForce = m_MinLaunchForce;
		m_AimSlider.value = m_MinLaunchForce;
	}

	private void Start()
	{
		m_FireButton = "Fire" + m_PlayerNumber;
		m_ChargeSpeed = (m_MaxLaunchForce - m_MinLaunchForce) / m_MaxChargeTime;
	}

	private void Update()
	{
		m_AimSlider.value = m_MinLaunchForce;

		//对开火逻辑每一帧都进行检测
		if (m_CurrentLaunchForce >= m_MaxLaunchForce && !m_Fired)//达到最大充能时间且尚未开火
		{
			m_CurrentLaunchForce = m_MaxLaunchForce;

			Fire();
		}
		else if (Input.GetButtonDown(m_FireButton))//按下开火按钮,开始充能
		{
			m_Fired = false;
			m_CurrentLaunchForce = m_MinLaunchForce;

			m_ShootingAudio.clip = m_ChargingClip;
			m_ShootingAudio.Play();
		}
		else if (Input.GetButton(m_FireButton) && !m_Fired)//长摁开火按钮进行充能且尚未开火
		{
			m_CurrentLaunchForce += m_ChargeSpeed * Time.deltaTime;

			m_AimSlider.value = m_CurrentLaunchForce;
		}
		else if (Input.GetButtonUp(m_FireButton) && !m_Fired)//放开开火按钮且尚未开火
		{
			Fire();
		}

完成效果图

6.1 项目进度图

存在的问题:

1.发现Shell所带的粒子特效子物体没有正确销毁,可能与弃用的duration有关

第七部分:游戏管理器

SpawnPoint

此游戏对象用于标识Tank生成点,当游戏开始时,会在指定的SpawnPoint上生成玩家的坦克

1.将SpawnPoint建立在地图上的适宜位置
2.通过Gizmo标识指定的颜色,使之在窗口中易于识别

屏幕UI

1.创建新的画布MessageCanvas
2.创建Text组件,并通过锚点设置限定适宜的范围。选择适宜的字体和字体颜色,使之居中
3.在Text组件设置中,通过Best Fit使之达到适宜的大小
4.为Text添加Shadow组件,设置适宜的颜色

完善相机控制

为了仅通过代码来控制相机追踪的Target,在脚本中,通过语句[HideInInspector]将Target数组隐藏

脚本

TankManager

此脚本用于管理坦克的多种设置(注:该脚本不继承自MonoBehavior,但有[Serializable])
与GameManager一同控制坦克的行为以及在各个游戏阶段玩家是否能对坦克进行控制

具体编程实现如下:

[Serializable]
public class TankManager
{
	public Color m_PlayerColor;								//玩家颜色
	public Transform m_SpawnPoint;							//玩家出生点
	[HideInInspector] public int m_PlayerNumber;			//玩家编号
	[HideInInspector] public string m_ColoredPlayerText;	//? 玩家颜色相关字符串
	[HideInInspector] public GameObject m_Instance;			//玩家坦克实例引用
	[HideInInspector] public int m_Wins;					//玩家当前胜利次数

	private TankMovement m_Movement;						//坦克移动脚本引用
	private TankShooting m_Shooting;						//坦克射击脚本引用
	private GameObject m_CanvasGameObject;					//坦克UI引用

	public void SetUp()//首次创建坦克时被调用
	{
		m_Movement = m_Instance.GetComponent<TankMovement>();
		m_Shooting = m_Instance.GetComponent<TankShooting>();
		m_CanvasGameObject = m_Instance.GetComponentInChildren<Canvas>().gameObject;

		m_Movement.m_PlayerNumber = m_PlayerNumber;
		m_Shooting.m_PlayerNumber = m_PlayerNumber;

		m_ColoredPlayerText = "<color=#" + ColorUtility.ToHtmlStringRGB(m_PlayerColor) + ">PLAYER" + m_PlayerNumber + "</color>";
		//? 使用html富文本来进行着色

		MeshRenderer[] renderers = m_Instance.GetComponentsInChildren<MeshRenderer>();
		//遍历坦克组分子物体中的所有网格渲染器,然后全部进行指定颜色的着色

		for (int i = 0; i < renderers.Length; i++)
		{
			renderers[i].material.color = m_PlayerColor;
		}
	}

	public void DisableControl()
	{
		m_Movement.enabled = false;
		m_Shooting.enabled = false;

		m_CanvasGameObject.SetActive(false);
	}

	public void EnableControl()
	{
		m_Movement.enabled = true;
		m_Shooting.enabled = true;

		m_CanvasGameObject.SetActive(true);
	}

	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);
	}
}

GameManager

此空游戏对象的同名脚本用于管理游戏逻辑

具体编程实现如下:

public class GameManager : MonoBehaviour
{
	public int m_NumRoundsToWin = 5;				//胜利所需回合数
	public float m_StartDelay = 3f;					//开始延迟
	public float m_EndDelay = 3f;					//结束延迟
	public CameraControl m_CameraControl;			//摄像机控制脚本引用
	public Text m_MessageText;						//屏幕UI文本引用
	public GameObject m_TankPrefab;					//坦克预制体引用
	public TankManager[] m_Tanks;                   //玩家坦克管理员数组

	private int m_RoundNumber;						//当前回合数
	private WaitForSeconds m_StartWait;				//开始延迟时间
	private WaitForSeconds m_EndWait;               //结束延迟时间
	private TankManager m_RoundWinner;              //当前回合胜利者
	private TankManager m_GameWinner;               //游戏胜利者

	private void Start()
	{
		//延迟时间赋值
		m_StartWait = new WaitForSeconds(m_StartDelay);
		m_EndWait = new WaitForSeconds(m_EndDelay);

		SpawnAllTanks();
		SetCameraTargets();

		StartCoroutine(GameLoop());//开启协程
	}

	private void SpawnAllTanks()//生成所有坦克
	{
		for (int i = 0; i < m_Tanks.Length; i++)
		{
			m_Tanks[i].m_Instance = 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()//设置镜头目标
	{
		Transform[] targets = new Transform[m_Tanks.Length];

		for (int i = 0; i < targets.Length; i++)
		{
			targets[i] = m_Tanks[i].m_Instance.transform;
		}

		m_CameraControl.m_Targets = targets;
	}

	private IEnumerator GameLoop()//游戏循环
	{
		yield return StartCoroutine(RoundStarting());//
		yield return StartCoroutine(RoundPlaying());//
		yield return StartCoroutine(RoundEnding());//

		if (m_GameWinner != null)//存在胜利者时
		{
			//Application.loadLevel(Application.loadedLevel);//该方法已过时
			SceneManager.LoadScene(SceneManager.GetActiveScene().name);//重载场景,重新开始游戏
		}
		else//尚未诞生胜利者
		{
			StartCoroutine(GameLoop());//开启下一轮游戏
		}
	}

	private IEnumerator RoundStarting()
	{
		ResetAllTanks();
		DisableTankControl();

		m_CameraControl.SetStartPositionAndSize();

		m_RoundNumber++;
		m_MessageText.text = "ROUND" + m_RoundNumber;

		yield return m_StartWait;
	}

	private IEnumerator RoundPlaying()
	{
		EnableTankControl();

		//m_MessageText.text = "";
		m_MessageText.text = string.Empty;

		while (!OneTankLeft())
		{
			yield return null;
		}
	}

	private IEnumerator RoundEnding()
	{
		DisableTankControl();

		m_RoundWinner = null;//清楚上一轮的胜利者,直到检查本轮胜利者后为其重新赋值

		m_RoundWinner = GetRoundWinner();

		if (m_RoundWinner != null)
			m_RoundWinner.m_Wins++;//胜者胜点增加

		m_GameWinner = GetGameWinner();

		string message = EndMessage();
		m_MessageText.text = message;



		yield return m_EndWait;
	}

	private void ResetAllTanks()//重置所有坦克
	{
		for (int i = 0; i < m_Tanks.Length; i++)
		{
			m_Tanks[i].Reset();
		}
	}

	private void DisableTankControl()//阻断玩家对坦克的控制
	{
		for (int i = 0; i < m_Tanks.Length; i++)
		{
			m_Tanks[i].DisableControl();
		}
	}

	private void EnableTankControl()//允许玩家对坦克的控制
	{
		for (int i = 0; i < m_Tanks.Length; i++)
		{
			m_Tanks[i].EnableControl();
		}
	}

	private bool OneTankLeft()//判断是否有坦克被击毁
	{
		int numTanksLeft = 0;//存活坦克数量

		for (int i = 0; i < m_Tanks.Length; i++)
		{
			if (m_Tanks[i].m_Instance.activeSelf)
				numTanksLeft++;
		}

		return numTanksLeft <= 1;
	}

	private TankManager GetRoundWinner()//返回本轮胜利的坦克
	{
		for (int i = 0; i < m_Tanks.Length; i++)//遍历坦克序列
		{
			if (m_Tanks[i].m_Instance.activeSelf)//找到仍生还的坦克(以双人游戏为例)
				return m_Tanks[i];
		}

		return null;//若为平局,则返回null
	}

	private TankManager GetGameWinner()//返回本次游戏胜利的坦克
	{
		for (int i = 0; i < m_Tanks.Length; i++)//遍历坦克序列
		{
			if (m_Tanks[i].m_Wins == m_NumRoundsToWin)//存在坦克胜点达到要求
				return m_Tanks[i];
		}

		return null;
	}

	private string EndMessage()//返回游戏结束时需要显示的文本
	{
		string message = "DRAW!";

		if (m_RoundWinner != null)
			message = m_RoundWinner.m_ColoredPlayerText + "WINS THE ROUND!";

		message += "\n\n\n\n";

		for (int i = 0; i < m_Tanks.Length; i++)
		{
			message += m_Tanks[i].m_ColoredPlayerText + ":" + m_Tanks[i].m_Wins + "WINS\n";
		}

		if (m_GameWinner != null)
			message = m_RoundWinner.m_ColoredPlayerText + "WINS THE GAME!";


		return message;
	}
}

完成效果图

7.1 项目进度图
7.2 项目进度图

存在的问题:

1.GameManager中协程的应用
2.为何使用TankManager这样不进行挂载的脚本
3.TankManager的Reset方法中,为何关闭再激活坦克实例引用

第八部分:音频混合

背景音乐

在GameManager上添加Audio Source组件,选择Background Music作为声音片段,并设置循环播放

声音混合器

1.创建一个Audio Mixer用于声音的控制
2.创建相应的分组,并将相应的音源输出连接到分组上
3.为背景音乐所在的输出组添加Dock Volume,并在音效所在输出组中添加Send到前者中,以回避背景音乐和音效的冲突

完成效果图

8.1 项目进度图

存在的问题:

1.unity音频的基本知识

第九部分:完善游戏(待补充)

目标

游戏开始界面与基本设置

游戏暂停选项与基本设置

游戏结束与计分板(需要对游戏原本的重复循环进行改动)(可能考虑实现的功能:玩家信息的存储)

额外的玩法(如ML-Agents训练的AI坦克)

第十部分:ML-Agents训练敌对坦克AI

设计思路

可能具备的能力

1.控制自身自由运动(v)
2.控制开火以及蓄力的时间(投射距离)(v)
3.追击敌对目标(?)
4.规避敌对目标的伤害(x)

需要的Observations

1.自身的位置、旋转信息
2.设计蓄力时间
3.敌对目标的位置、旋转信息

控制 AgentAction

这里采用离散动作空间,三个分支分别控制垂直移动、水平转向以及开火

具体编程实现

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

public class TankAgent : Agent
{
	public int m_PlayerNumber = 1;//玩家编号

	public CameraControl m_CameraControl;//摄像机控制脚本引用

	public Rigidbody TankRig;//坦克刚体组件引用
	public GameObject Opponent;//坦克敌对目标位置引用

	public Transform spawnPoint;//出生点

	private GameObject m_CanvasGameObject;//坦克UI引用
	private TankMovement tankMovement;
	private TankShooting tankShooting;
	private TankHealth tankHealth;

	private float time = 0;

	public override void InitializeAgent()
	{
		tankMovement = GetComponent<TankMovement>();
		tankShooting = GetComponent<TankShooting>();
		tankHealth = GetComponent<TankHealth>();
		m_CanvasGameObject = GetComponentInChildren<Canvas>().gameObject;
		base.InitializeAgent();
		TankRig = GetComponent<Rigidbody>();

		tankMovement.m_PlayerNumber = m_PlayerNumber;
		tankShooting.m_PlayerNumber = m_PlayerNumber;

		m_CameraControl.SetStartPositionAndSize();
	}

	

	public override void CollectObservations()//收集观察向量
	{
		base.CollectObservations();
		AddVectorObs(Opponent.transform.localPosition.x);//对手的位置信息x
		AddVectorObs(Opponent.transform.localPosition.z);//对手的位置信息z
		AddVectorObs(Opponent.transform.localRotation.eulerAngles);//对手的旋转
		AddVectorObs(transform.localPosition.x);//自身的位置x
		AddVectorObs(transform.localPosition.z);//自身的位置z
		AddVectorObs(transform.localRotation.eulerAngles);//自身的旋转
		AddVectorObs(tankShooting.m_CurrentLaunchForce);//开火的蓄力时长(1)

	}

	public override void AgentAction(float[] vectorAction, string textAction)
	{
		base.AgentAction(vectorAction, textAction);

		var Vertical = (int)vectorAction[0];// 决策向量0
		var Horizontal = (int)vectorAction[1];// 决策向量1
		var FireButton = (int)vectorAction[2];// 决策向量2
		//var类型预先不用知道变量的类型,根据传入的变量的值转换为相应类型。必须在定义时完成赋值,且不能再次赋值

		switch (Vertical)//根据决策向量的值来决定坦克的移动
		{
			case 0:
				tankMovement.m_MovementInputValue = 0;// ?
				break;
			case 1:
				tankMovement.m_MovementInputValue = 1;// ?
				break;
			case 2:
				tankMovement.m_MovementInputValue = -1;// ?
				break;
		}

		switch (Horizontal)//根据决策向量的值决定坦克的转向
		{
			case 0:
				tankMovement.m_TurnInputValue = 0;// ?
				break;
			case 1:
				tankMovement.m_TurnInputValue = 1;// ?
				break;
			case 2:
				tankMovement.m_TurnInputValue = -1;// ?
				break;
		}

		switch (FireButton)//根据决策向量的值决定坦克是否开火
		{
			case 0:
				//tankShooting.m_GetFireButton = false;// ?
				//Input.GetButtonDown(tankShooting.m_FireButton);
				tankShooting.FireValue = -1.0f;
				break;
			case 1:
				//tankShooting.m_GetFireButton = true;// ?
				//Input.GetButton(tankShooting.m_FireButton);
				tankShooting.FireValue = 0f;
				break;
			case 2:
				//Input.GetButtonUp(tankShooting.m_FireButton);
				tankShooting.FireValue = 1.0f;
				break;
		}

		if (Opponent.gameObject.activeSelf == false)//敌对目标被消灭
		{
			Debug.Log(m_PlayerNumber + "号玩家被消灭!");
			SetReward(1.0f);
			Done();
		}

		if (Opponent.GetComponent<TankHealth>().m_UnderAttack)
		{
			Debug.Log(m_PlayerNumber + "号玩家击中了对手!");
			SetReward(0.2f);
			//Opponent.GetComponent<TankHealth>().m_UnderAttack = false;
		}

		if (tankHealth.m_UnderAttack)//自身遭受攻击
		{
			Debug.Log(m_PlayerNumber + "号玩家遭到攻击!");
			SetReward(-0.05f);
			tankHealth.m_UnderAttack = false;//重置TankShooting脚本中的受击布尔变量

			if (!gameObject.activeSelf)//自身被摧毁
			{
				Debug.Log(m_PlayerNumber + "号被对手消灭了!");
				//SetReward(-0.01f);
				Done();
			}
		}

		if (Opponent.GetComponent<TankHealth>().m_UnderAttack)
		{
			SetReward(0.1f);
		}

		time += Time.deltaTime;//限定动作时间,未能击杀则惩罚(效果不佳)

		//if (time >= 20f)
		//{
		//	SetReward(-0.01f);
		//	Done();
		//}
	}

	public override void AgentReset()
	{
		base.AgentReset();

		DisableControl();
		
		gameObject.SetActive(false);
		gameObject.SetActive(true);

		Opponent.SetActive(true);
		m_CameraControl.SetStartPositionAndSize();
		EnableControl();
		//transform.position = new Vector3(Random.Range(-30f, 30f), 0, Random.Range(-30f, 30f));//使AI坦克随机生成在场上一定范围内的某个位置
		transform.position = spawnPoint.position;
		//transform.rotation = Quaternion.Euler(0f, Random.Range(0.0f, 360.0f), 0f);//使AI坦克获得随机的初始方向(通过旋转)
		transform.rotation = spawnPoint.rotation;

	}

	public void DisableControl()
	{
		tankMovement.enabled = false;
		tankShooting.enabled = false;

		m_CanvasGameObject.SetActive(false);
	}

	public void EnableControl()
	{
		tankMovement.enabled = true;
		tankShooting.enabled = true;

		m_CanvasGameObject.SetActive(true);
	}
}

训练效果

可以进行基本的操纵,但未能很好地实现训练目标,且训练次数较高时会因不明原因崩溃。一方面关于"对抗"式的ML-Agents相关知识了解不足,使用一个Brain控制相同代码的Aent进行对抗可能并非较好的设计方案。另一方面对机器学习的知识极其缺乏,在诱导Agent实现目标时未能设计足够有效的奖惩方案。

  • 7
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以上是资源连接 ! Unity 5.X 3D游戏开发技术详解与典型案例Unity 3D集成开发环境界面、脚本的编写和众多高级特效的实现进行了详细介绍,内容深入浅出,是一本适合不同需求、不同开发水平读者的技术宝典。 全书共分16章。第1章主要介绍了Unity 3D的诞生、特点、开发环境的搭建及运行机制;第2章对Unity 3D集成开发环境进行了详细介绍;第3章介绍了Unity 3D中脚本的编写;第4章主要对Unity 3D开发过程中经常使用的组件及对象进行了详细介绍;第5章介绍了Unity游戏开发中非常流行的第三方UI界面开发组件库—NGUI的基础知识;第6章介绍了Unity开发平台的完整的物理引擎体系;第7章介绍了Unity 3D中的着色器和着色器语言—ShaderLab;第8章介绍了天空盒、虚拟按钮与摇杆、声音、水特效、3D拾取、重力加速度传感器及雾特效等开发常用的技术;第9章介绍了Unity中经常使用的光影效果,主要包括各种光源、光照烘焙、法线贴图、镜面特效、波动水面真实效果等技术;第10章介绍了Unity中模型的网格概念及新旧动画系统;第11章介绍了Unity自带的地形引擎、拖尾渲染及导航网格和寻路系统等知识;第12章介绍了AssetBundle更新资源包的使用;第13章介绍了Unity中的多线程技术与网络开发;第14章介绍了Unity 2D游戏开发工具;第15章介绍了Unity 3D提供的Profiler工具的使用方法,及断点调试的两种方式;第16章介绍了完整的大型3D游戏案例—指间足球。
纯Python开发的大型游戏较为罕见,因为Python作为一种高级编程语言,运行效率相对较低,不太适合处理大规模游戏的图形和物理计算。然而,Python在游戏开发中仍然扮演着重要的角色,通常用于开发游戏的服务器端和游戏工具。 一个著名的纯Python开发游戏是《Eve Online》,它是一个大型的多人在线游戏,使用Python作为开发语言编写服务器和客户端的一部分。Python的简洁和灵活性使得其在游戏逻辑和服务器架构方面表现出色。另外一个例子是《World of Tanks》,这个游戏的服务器端也是使用Python编写的。 此外,还有许多知名游戏使用Python作为扩展脚本语言,用于游戏中的逻辑和工具开发。其中包括《洛克人:传奇》、《文明V》和《辐射3》等。这些游戏使用C++等其他语言开发游戏引擎和核心功能,而Python用作脚本语言来实现游戏中的各种功能和逻辑。 总的来说,纯Python开发的大型游戏比较少见,但Python在游戏开发中仍然发挥着重要的作用,尤其在服务器端和游戏工具方面。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【python游戏开发】:那些由python开发的知名网站和游戏你知道多少](https://blog.csdn.net/weixin_49895216/article/details/127339944)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值