【2020.1.12】
本案例来自unity官方中级游戏教程Tanks(单机双人坦克大战)
项目同时用于Unity机器学习内容的学习,预定计划为训练可规避障碍物,欲图消灭玩家的坦克ai
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
此脚本用于控制坦克的移动,其具体功能为:
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);//?
}
1.坦克旋转控制相关方法(向量、欧拉角、四元数)的有关知识
2.关于代码中变量的命名前缀"m_"
这是C#中的一种变量命名规范,m意为member,表示变量是该类成员变量
1.创建空物体CameraRig,用于摄像机的控制,调整其到合适的角度
2.将Camera设置为CameraRig的子物体(此时Camera的位置信息将以CameraRig为中心),调整其坐标到适宜位置
此脚本用于控制摄像机的移动,注意,脚本附着于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();//获取并设置目标尺寸
}
1.关于SmoothDamp方法中,第三个参数的意义
2.对于通过FindRequiredSize方法实现正交摄像机调焦的逻辑尚不理解
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
此脚本用于锁定坦克生命条的旋转
具体编程实现如下:
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
此脚本用于控制坦克的血条变化(注意添加至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,不进行摧毁)
}
1.需要系统学习UGUI的相关知识
2.需要系统学习粒子系统的相关知识
导入Model文件夹中的Shell模型
为Shell添加Capsule Collider,并设置为触发器模式(Is Trigger)(子弹生效不需要其本身与外界发生直接的物理交互)
适当调整触发器的中心相对坐标、半径、高度以及方向
为Shell添加RigBody组件,基本设置不变
1.为Shell添加预制体ShellExplosion作为子物体
2.为ShellExplosion添加Audio Source组件以实现子弹爆炸音效(声音片段为ShellExplosion)
为Shell添加Light组件
此脚本用于处理子弹爆炸的相关逻辑(注意保存后在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;
}
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前端,适当抬高高度
此脚本用于处理子弹发射相关逻辑
具体编程实现如下:
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();
}
1.发现Shell所带的粒子特效子物体没有正确销毁,可能与弃用的duration有关
此游戏对象用于标识Tank生成点,当游戏开始时,会在指定的SpawnPoint上生成玩家的坦克
1.将SpawnPoint建立在地图上的适宜位置
2.通过Gizmo标识指定的颜色,使之在窗口中易于识别
1.创建新的画布MessageCanvas
2.创建Text组件,并通过锚点设置限定适宜的范围。选择适宜的字体和字体颜色,使之居中
3.在Text组件设置中,通过Best Fit使之达到适宜的大小
4.为Text添加Shadow组件,设置适宜的颜色
为了仅通过代码来控制相机追踪的Target,在脚本中,通过语句[HideInInspector]将Target数组隐藏
此脚本用于管理坦克的多种设置(注:该脚本不继承自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 = " + ColorUtility.ToHtmlStringRGB(m_PlayerColor) + ">PLAYER" + m_PlayerNumber + " ";
//? 使用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);
}
}
此空游戏对象的同名脚本用于管理游戏逻辑
具体编程实现如下:
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;
}
}
1.GameManager中协程的应用
2.为何使用TankManager这样不进行挂载的脚本
3.TankManager的Reset方法中,为何关闭再激活坦克实例引用
在GameManager上添加Audio Source组件,选择Background Music作为声音片段,并设置循环播放
1.创建一个Audio Mixer用于声音的控制
2.创建相应的分组,并将相应的音源输出连接到分组上
3.为背景音乐所在的输出组添加Dock Volume,并在音效所在输出组中添加Send到前者中,以回避背景音乐和音效的冲突
1.unity音频的基本知识
1.控制自身自由运动(v)
2.控制开火以及蓄力的时间(投射距离)(v)
3.追击敌对目标(?)
4.规避敌对目标的伤害(x)
1.自身的位置、旋转信息
2.设计蓄力时间
3.敌对目标的位置、旋转信息
这里采用离散动作空间,三个分支分别控制垂直移动、水平转向以及开火
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().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实现目标时未能设计足够有效的奖惩方案。