初学unity之3D坦克大战
1、导入资源
从unity store中下载所需资源内含坦克模型、弹药模型、地图模型…然后将资源导入unity
2、 创建属于自己的文件夹
主要包括三个文件夹
1、Scripts
2、Prefabs
3、Scenes
3、创建场景
- 在Scenes文件夹新建场景3DTanksBattle
- 将资源中的地图拖入3DTB
- 将坦克和子弹模型拖入3DTB
4、制作炮弹预制件
-
添加Rigidbody刚体
模拟炮弹物理作用,有质量和重力。 -
Add Component->Physics->RigidBody
-
添加Collider碰撞体
Add Component->Physics->Capsule Collider(胶囊碰撞体)
调整胶囊的轴向、高度、半径以贴合炮弹模型
-
添加炮弹爆炸粒子(particles)特效
从资源中找到爆炸特效拖入shell对象中
5、取消我自己的预制件与资源的关联
1、右键Hierarchy中的预制件->Prefabs->unpack
2、成功(Hierarchy中字体由蓝变白),这样对物体的操作就不会有干扰
3、将Hierarchy中处理好的预制件拖入Project中自己创建的文件夹Prefabs中
6、制作坦克预制件
-
添加RigidBody刚体
-
添加Box Collider碰撞体
坦克分上下两部分,添加两个碰撞体以贴合坦克形状 -
添加坦克粒子爆炸特效
-
添加坦克血条
1、添加UI控件:右键Tank->UI->Slider(滑动块)
2、 Fill Area的Fill Rect改为full,360°,Fill移入Slider并删除Fill Area
3、Background和Fill图片都设为圆环
Background和Fill坐标全部置零使二者重合
调色
Slider设为长宽4x4大小
-
做成预制件拖入Prefabs文件夹
7、调整光线
1、Window->Rendering->Lighting进入光源设置
2、在Environment Lighting将Source的SkyBox改为Color
3、在Scene->Lighting Settings-->new Lighting Settings
4、LightingMapping Seethings将LightingMapper设为Enlighten
5、Lighting Mode设为Subtractive
6、删除原先场景光照
8、使用脚本控制坦克行为
- 在Scripts文件夹新建脚本,命名为TankControl
使用Input控制器结合RigidBody让坦克移动
//对象声明过后要实例化才能使用
//变量名不要使用rigidbody,这个命名在基类中存在所以会被隐藏继承,除非加new--用下划线区分开
public Rigidbody _rigidbody;
// Start is called before the first frame update
void Start()
{
//this指这个脚本,此脚本的游戏对象(挂载到Tank),获取此游戏对象身上的RigidBody组件
_rigidbody = gameObject.GetComponent<Rigidbody>();//新版本不必加this
}
- Input管理器
1、打开Input Manager
2、edit->Project Settings->Input Manager
3、Horizontal(水平)、Vertical(垂直)—
public float speed = 2;//速度
public float rotateSpeed = 2;//转速
void Update()
{
//结合Input控制器
h_Value = Input.GetAxis("Horizontal1");//a,d--转弯
v_Value = Input.GetAxis("Vertical1");//s,w--前进后退
if (v_Value != 0)//为正,前进,为负,后退
{
_rigidbody.MovePosition(this.transform.position + v_Value*this.transform.forward * speed * Time.deltaTime);//Time.deltaTime--上一帧执行的时间/每帧的间隔时间
}
//转弯
if(h_Value!=0)
{
if(v_Value<0)
{
h_Value = -h_Value;//转弯逻辑
}
this.gameObject.transform.Rotate(Vector3.up * h_Value * rotateSpeed * Time.deltaTime);
}
}
- 避免坦克翻车
改变x轴和z轴
在RigidBody约束(Constraints)轴变化
- 两个坦克
枚举两个坦克
public enum TankType
{
Tank_One=1,
Tank_Two=2,
Tank_Enemy=3,
}
public TankType tankType = TankType.Tank_One;//坦克1
public string inputHorizontalStr;
public string inputVerticalStr;//使输入方式可变,wasd和上下左右
void Start()
{
//this指这个脚本,此脚本的游戏对象(挂载到Tank),获取此游戏对象身上的RigidBody组件
_rigidbody = gameObject.GetComponent<Rigidbody>();//新版本不必加this
inputHorizontalStr = inputHorizontalStr + (int)tankType;
//在unity面板中输入Horizontal和Vertical,自动根据坦克1或2来赋值Horizontal1/2和Vertical1/2
inputVerticalStr = inputVerticalStr + (int)tankType;
}
void Update()
{
//结合Input控制器
h_Value = Input.GetAxis(inputHorizontalStr;//a,d--转弯
v_Value = Input.GetAxis(inputVerticalStr);//s,w--前进后退
}
- 炮弹发射
InputManager中Fire,FIre1指向空格键
public GameObject shell;//炮弹
public Transform shellPos;//炮弹发射位置
void Start()
{
inputFireStr = inputFireStr + (int)tankType;
}
void Update()
{
//取得键盘的空格键输入---开炮,多种写法
//if (Input.GetKeyDown(KeyCode.Space)) { }
if(Input.GetButtonDown(inputFireStr))
{
//写开炮方法
OpenFire();
}
}
在unity为Tank添加一个游戏对象shellPos作为炮弹的发射位置
由RigidBody给炮弹速度
void OpenFire()
{
//1、先克隆一个炮弹
//2、给炮弹一个速度
if (shell != null)
{
GameObject shellObj = Instantiate(shell, shellPos.position, shellPos.rotation);//克隆一个原始对象并返回这个克隆体
//shell克隆到shellPos的位置
Rigidbody shellRigidbody = shellObj.GetComponent<Rigidbody>();//克隆体shell获取shell身上的RigidBody
if(shellRigidbody!=null)
{
shellRigidbody.velocity = shellPos.forward * shellSpeed;//沿shellSpeed速度向前---矢量速度
}
}
}
- 炮弹发射力度(优化)
— —键盘按下的状态
定义炮弹的最小速度、当前速度、最大速度
//按下的当时
if(Input.GetButtonDown(inputFireStr))
{
//写开炮方法
//OpenFire();
IsFire = true;//按下,发射
currentSpeed = MinSpeed;
}
//按下的状态
if(Input.GetButton(inputFireStr)&&IsFire)
{
currentSpeed += speedChange + Time.deltaTime;
if(currentSpeed>=MaxSpeed)
{
currentSpeed = MaxSpeed;
OpenFire(currentSpeed);
currentSpeed = MinSpeed;
IsFire = false;
}
if (Input.GetButtonUp(inputFireStr) && IsFire)
{
OpenFire(currentSpeed);
currentSpeed = MinSpeed;
IsFire = false;
}
}
9、脚本控制炮弹行为
- 给shell添加脚本ShellControl
- 物理碰撞:碰撞双方都需要挂载Collider组件
private void OnCollisionEnter(Collision collision)//当此刚体接触到另一刚体,就调用
{
if (shellExplosion != null)
{
shellExplosion.Play();//播放爆炸
//在unity编辑器拖拽初始化shellExplosion
}
Destroy(this.gameObject);//碰撞完销毁
}
缺陷:粒子特效是炮弹的子物体,销毁炮弹同时也销毁粒子特效,所以无法表现出效果,改进
private void OnCollisionEnter(Collision collision)//当此刚体接触到另一刚体,就调用
{
shellExplosion.transform.parent = null;//父物体设为null
if (shellExplosion != null)
{
shellExplosion.Play();//播放爆炸
//在unity编辑器拖拽初始化shellExplosion
Destroy(shellExplosion.gameObject, shellExplosion.main.duration);
}
Destroy(this.gameObject);//碰撞完销毁
//print("shabi");
}
- 坦克被击中的爆炸力
Layer图层:将一组行为类似的对象放在一起,按照某种方式处理 常见用法: 1、在Scene中用Layer选择哪些图层可出现在Scene视图中; 2、排除不被灯光照亮的对象 ; 3、告诉unity哪些对象之间可以进行物理交互; 4、使用图层定义摄像机可以看到什么以及不能看到什么;
private void OnCollisionEnter(Collision collision)//当此刚体接触到另一刚体,就调用
{
//中心点,围绕半径,获取范围内Collider图层
Collider[] tankCollider = Physics.OverlapSphere(this.transform.position,explosionRadius,tankMask);//返回一个数组,碰撞范围
for(int i=0;i<tankCollider.Length;i++)
{
var tankRigidBody= tankCollider[i].gameObject.GetComponent<Rigidbody>();
if(tankRigidBody!=null)
{
tankRigidBody.AddExplosionForce(explosionForce,this.transform.position,explosionRadius);//添加爆炸力:爆炸力,爆炸中心点,爆炸半径
}
}
}
- 伤害
计算血量
private void OnCollisionEnter(Collision collision)//当此刚体接触到另一刚体,就调用
{
float distance = (this.transform.position - tankCollider[i].gameObject.transform.position).magnitude;//三元数的模,中心坐标减去爆炸体坐标
float currentDamage = distance / explosionRadius * MaxDanage;
}
在坦克脚本添加坦克的PH值
public float PH = 15;//坦克血量
//炮弹伤害
public void ShellDamage(float damage)
{
if(PH>0)
{
PH -= damage;//血量减伤害
}
if(PH<=0)
{
//死掉
}
}
//炮弹脚本
private void OnCollisionEnter(Collision collision)//当此刚体接触到另一刚体,就调用
{
float distance = (this.transform.position - tankRigidBody.position).magnitude;//三元数的模,中心坐标减去爆炸体坐标
float currentDamage = distance / explosionRadius * MaxDanage;
var tankControl=tankCollider[i].gameObject.GetComponent<TankControl>();
if(tankControl!=null)
{
tankControl.ShellDamage(currentDamage);//造成伤害
}
}
坦克身上有两个碰撞体,爆炸会产生两端伤害
所以,去掉一个碰撞体(Box Collider)
然后在Tank新建子游戏对象作为碰撞体(Collider),并且与坦克不在同一图层(Layer)
同步到预制件
- 血条(Slider)同步
控制Slider(UI空间) 的value值
public Slider phSlider;//实例化后就可获得value的值
void Start()
{
//血量初始化
phSlider.maxValue = PH;
phSlider.value = PH;
}
public void ShellDamage(float damage)
{
if(PH>0)
{
PH -= damage;//血量减伤害
phSlider.value = PH;//血量减少
}
if(PH<=0)
{
//死掉
//坦克爆炸
}
}
坦克死亡爆炸
public ParticleSystem tankExplosion;//实例化
if(PH<=0)
{
//死掉
//坦克爆炸
if(tankExplosion!=null)
{
tankExplosion.transform.parent = null;
tankExplosion.Play();//播放粒子特效
Destroy(tankExplosion.gameObject, tankExplosion.main.duration);
}
this.gameObject.SetActive(false);//隐藏坦克,直接销毁影响后面再次使用
}
10、坦克的产生和初始化
- 在不同位置加载两个坦克
新建脚本GameManager以及相应游戏对象
新建两个游戏对象作为坦克刷新坐标,初始化
void TankSpwan()
{
GameObject tankOne = Instantiate(tankPrefab, PosOne.position, PosOne.transform.rotation);//克隆坦克作为坦克一号,位置在PosOne
var tankOneControl = tankOne.GetComponent<TankControl>();//获取TankControl的组件,为了坦克类型
if(tankOneControl!=null)
{
tankOneControl.tankType = TankType.Tank_One;
}
GameObject tankTwo = Instantiate(tankPrefab, PosTwo.position, PosTwo.transform.rotation);
var tankTwoControl = tankTwo.GetComponent<TankControl>();//获取TankControl的组件,为了坦克类型
if (tankTwoControl != null)
{
tankTwoControl.tankType = TankType.Tank_Two;
}
}
- 设置坦克颜色
坦克颜色材质
void TankSpwan()
{
print("shabi");
GameObject tankOne = Instantiate(tankPrefab, PosOne.position, PosOne.transform.rotation);//克隆坦克作为坦克一号,位置在PosOne
var tankOneControl = tankOne.GetComponent<TankControl>();//获取TankControl的组件,为了坦克类型
if(tankOneControl!=null)
{
tankOneControl.tankType = TankType.Tank_One;
MeshRenderer tankOneRenderers = tankOne.GetComponentInChildren<MeshRenderer>();//获取材质
tankOneRenderers.material.color = tankOneColor;
}
GameObject tankTwo = Instantiate(tankPrefab, PosTwo.position, PosTwo.transform.rotation);
var tankTwoControl = tankTwo.GetComponent<TankControl>();//获取TankControl的组件,为了坦克类型
if (tankTwoControl != null)
{
tankTwoControl.tankType = TankType.Tank_Two;
MeshRenderer tankTwoRenderers = tankTwo.GetComponentInChildren<MeshRenderer>();//获取材质
tankTwoRenderers.material.color = tankTwoColor;
}
}
}
PS:未能解决获取坦克下所有子对象的材质进行颜色修改....后续在研究
PSS:当游戏运行后,会出现“ArgumentException: Input Axis Horizontal12 is not setup. To change the input settings use: Edit -> Settings -> Input”的报错,因为坦克类型在声明时被初始化为坦克1,后面调用是在Horizontal1/Vertical1/Fire1的条件下再进行坦克类型判断,去掉初始化即可。
11、相机位置跟随
- 主摄像机改为正交模式,使视野变为平行
- 为主摄像机创建脚本
让相机跟随两个坦克的中心点移动
public GameObject[] tanks;//两个坦克的数组
public Vector3 targetCameraPos = Vector3.zero;
public Camera mainCamera;
public Vector3 currentVelocity = Vector3.zero;//初始化
public float smoothTime = 0.1f;//平滑时间
public float maxSmoothSpeed = 2;
// Start is called before the first frame update
void Start()
{
mainCamera = Camera.main;//初始化
}
void Update()
{
ResetCameraPos();
}
void ResetCameraPos()
{
//1、计算相机移动的位置
//2、平滑移动相机
Vector3 sumPos=Vector3.zero;//初始化
foreach(var tank in tanks)//tanks中的每一辆坦克
{
sumPos += tank.transform.position;//坦克的位置全部相加
}
if(tanks.Length>0)
{
targetCameraPos= sumPos / tanks.Length;//算出相机移动目标位置
targetCameraPos.y = mainCamera.transform.position.y;//避免y坐标偏差
mainCamera.transform.position = Vector3.SmoothDamp(mainCamera.transform.position, targetCameraPos, ref currentVelocity, smoothTime, maxSmoothSpeed);
//当前位置,目标位置,速度
}
}
}
初始化坦克数组,在初始化坦克处赋值
Tank设置Tag为Player
public RyumnCameraControl cameraControl;//使运行后才出现的Tank(Clone)赋值给坦克数组
// Start is called before the first frame update
void Start()
{
TankSpwan();//初始化
if(cameraControl!=null)
{
cameraControl.tanks = GameObject.FindGameObjectsWithTag("Player");//查找复制后坦克的tag
}
}
先初始化生成坦克,再为坦克赋值
- 相机尺寸修改
void ResetCameraSize()
{
//通过计算相机与坦克中心的位置的x和z差值,来计算size值
//勾股
float size = 0;
foreach(var tank in tanks)
{
Vector3 offSetPos = tank.transform.position - targetCameraPos;
//坦克位置减去两坦克中心位置
float z_Value = Mathf.Abs(offSetPos.z);//绝对值
size = Mathf.Max(size, z_Value);//z方向取最大值
float x_Value = Mathf.Abs(offSetPos.x);
size = Mathf.Max(size, x_Value/mainCamera.aspect);
}
mainCamera.orthographicSize = size;//正交模式下相机的size
}
运行发现两个坦克一直处于画面边缘
修改 size += sizeOffset;
- 优化相机位置
给主相机创建父物体,通过控制父物体来控制主相机
将代码中的mainCamera对象修改为父物体12、游戏音效
- 新建游戏对象AudioGroup,添加子对象并添加Audio Source组件用来挂载音乐
对每个游戏对象挂载相应音频,创建脚本声明相应变量并初始化
- 新建游戏对象AudioGroup,添加子对象并添加Audio Source组件用来挂载音乐
public AudioSource bgm;
public AudioSource tankExplosion;
public AudioSource shellExplosion;
public AudioSource tankFire;
- 游戏开始时播放bgm
在坦克初始化脚本里
void Update()
{
audioManager = GameObject.FindWithTag("AudioManager").GetComponent<RyumnAudioManager>();
//为音频管理添加Tag,使用Tag查找
if(audioManager!=null)
{
audioManager.bgmAudioPlay();
}
}
- 坦克发射炮弹音效
//爆炸音效
public AudioSource shellFireAudio;
public AudioSource shellExplosionAudio;
void Start()
{
//坦克开火音效
if (shellFireAudio != null)
{
if (!shellFireAudio.isPlaying)
{
shellFireAudio.Play();
}
}
}
//炮弹爆炸音效
if (shellExplosionAudio != null)
{
shellExplosionAudio.gameObject.transform.parent = null;//父物体设为null
if (!shellExplosionAudio.isPlaying)
{
shellExplosionAudio.Play();
Destroy(shellExplosionAudio.gameObject, 1);
}
}
炮弹爆炸音效会跟随炮弹被销毁,因此听不见
将炮弹爆炸音效做粒子特效同样处理
13、给屏幕添加光晕效果
- 主相机添加组件:Post-process Layer,Layer设为Postprocessing
- 新建空游戏对象,添加Post-Processing Volume组件
14、重新加载
public void ShellDamage(float damage)
{
if(PH<=0)
{
Invoke("ReloadTankBattleScene", 2);
}
}
void ReloadTankBattleScene()
{
SceneManager.LoadScene("3DTanksBattle");
}
15、游戏发布
- File->Build Setting
一系列设置