【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);//?}

完成效果图

存在的问题:

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();//获取并设置目标尺寸}

完成效果图

存在的问题:

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

脚本

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,不进行摧毁)}

完成效果图

存在的问题:

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

第五部分:子弹

模型导入

导入Model文件夹中的Shell模型

碰撞体(触发器)组件

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

刚体组件

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

完成效果图

存在的问题:

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

完成效果图

存在的问题:

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

完成效果图


存在的问题:

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

第八部分:音频混合

背景音乐

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

声音混合器

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

完成效果图

存在的问题:

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);//对手的位置信息xAddVectorObs(Opponent.transform.localPosition.z);//对手的位置信息zAddVectorObs(Opponent.transform.localRotation.eulerAngles);//对手的旋转AddVectorObs(transform.localPosition.x);//自身的位置xAddVectorObs(transform.localPosition.z);//自身的位置zAddVectorObs(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];// 决策向量0var Horizontal = (int)vectorAction[1];// 决策向量1var 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实现目标时未能设计足够有效的奖惩方案。

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

  1. Unity3D游戏开发入门学习笔记

    学习内容概要: 软件面板功能.材质球.预制体.摄像机.灯光.鼠标键盘输入.组件.刚体.碰撞体.PC端游戏打包发布.... 第1课:课程介绍与Unity3D环境搭建 1.Unity3D,一个游戏开发引擎 ...

  2. Unity3D游戏开发工作学习随笔[序](主要讲述自己学习和工作的经历,先不谈技术和知识)

    起 虽然在前两年,21年大学课程结束,实习的时候,是入了Java开发的坑. 那时候没有想清楚,只是该学的学了,涉猎的多,但又没有专精,也没有仔细的思考以后的就业方向,也缺少对人生,理想,这些东西的方向 ...

  3. Unity 2D游戏开发案例学习——Robble Swifthand(下)

    10 2D灯光效果(法线贴图) 在这一节中,我们为场景添加一些装饰物件以及对应的灯光效果 添加火炬以及灯光 打开Props资源文件夹,找到我们需要的火炬预制体 Wall Torch,将父级火炬本身以及 ...

  4. 从一点儿不会开始——Unity3D游戏开发学习(一)

    一些废话 我是一个windows phone.windows 8的忠实粉丝,也是一个开发者,开发数个windows phone应用和两个windows 8应用.对开发游戏一直抱有强烈兴趣和愿望,但奈何 ...

  5. 【Unity3D游戏开发学习笔记】(六)上帝之手—GameObject的操作

    在Unity中,所有实体都属于游戏对象(GameObject),比如外部导入到场景中的模型,Unity自带的立方体等等,而要将这些GameOject进行管理,交互等操作,则需要用到脚本来实现,上一节我 ...

  6. unity3d什么的书籍比较好,unity3d游戏开发书籍汇总

    近年来,游戏行业出现了前所未有的震荡期,各种平台的涌现使得行业内部的竞争愈演愈烈.前几年,要想制作好的游戏,肯定就需要强大硬件的支持,所以大部分3D游戏都出现在PC或PS3.Xbox等专业游戏主机上, ...

  7. Unity3D游戏开发之仿仙剑奇侠传一2D游戏 (一)

    今天要和大家分享的是基于Unity3D开发2D游戏,博主一直钟爱于国产武侠RPG,这个我在开始写Unity3D游戏开发系列文章的时候就已经说过了,所以我们今天要做的就是利用Unity3D来实现在2D游 ...

  8. Unity3D游戏开发之邂逅Unity3D

    从今天起,博主决定要在毕业前把大学里想学的东西都学完.所以,从今天起,大家将看到由我为大家带来的Unity3D系列文章,让我们一起来学习Unity3D游戏开发吧! 在正式今天的文章之前,博主想简单介绍 ...

  9. Unity3D游戏开发-宣雨松读书摘要(2015-4-17 18:36)

    本书基于Unity3.5编写,通过丰富的游戏实例,以JavaScript与C#两种语言介绍Unity开发. Unity3D游戏开发-宣雨松 序 它支持JavaScript.C#.Boo三种脚本语言 ...

最新文章

  1. 基于 Docker 的 MySQL 导入导出数据
  2. java字符数统计_【JAVA300例】51、统计输入的字符串中各种字符的字符数
  3. win10打开程序响应很慢_小程序商城打开加载很慢?你上传的图片是不是太大了,压缩一下吧!...
  4. BAT脚本加防火墙455端口
  5. SQL笛卡尔积结合前后行数据的统计案例
  6. SAP Fiori Elements 应用里和 Fiori 3 相关的外观设置
  7. matlab java错误_求助:matlab load mat文件出错!java exception occurred:
  8. FFmpeg编码支持与定制(三)
  9. mysql5.7 yum 密码,CentOS 7.7解决yum方式安装的MySQL 5.7 root用户密码丢失问题
  10. 微信小程序-组件使用
  11. orbslam2初始化流程
  12. 51单片机间接寻址C语言,51单片机寄存器间接寻址方式与举例
  13. 高薪职位怎么找?你们来学学这3招
  14. win10邮箱怎么设置qq邮箱服务器地址,老鸟给你说win10自带邮件怎么添加qq邮箱的解决方式...
  15. MySQL【部署 04】8.0.25离线部署(下载+安装+配置)Failed dependencies 问题处理及8.0配置参数说明
  16. windows系统完全换ubuntu
  17. git学习三——打发布标签(tag)
  18. linux如何手动kill vnc端口并修改分辨率
  19. 4、ESP32-S - 连接 WiFi
  20. 用Python代码画出灰太狼

热门文章

  1. 这个【vue】项目,让我明白了…
  2. Python语法易混淆
  3. 蔓迪、落健、heybro、达霏欣哪个效果更好?自然选蔓迪
  4. 通过用jQuery写一个页面,我学到了什么
  5. 58同城自动登录功能 分享给大家!
  6. [风铃开发系列]IView动态菜单配置
  7. Affine-Transformation Parameters Regression for Face Alignment
  8. Linux 下串口编程(C++ 程序设计)
  9. 文本相似度:A Survey of Text Similarity Approaches
  10. 多部分元件原理图封装的画法