【Unity3D】Tank大战
1 需求实现
项目代码见→坦克大战1.1.0
1)人机交互
- 玩家通过 ↑ ↓ ← → 键(或 W、S、A、D)键控制己方坦克平移;
- 玩家通过滑动鼠标右键控制己方坦克左右旋转;
- 玩家通过鼠标左键(或空格键)控制己方坦克发射炮弹;
- 玩家通过 ESC 键控制窗口全屏和恢复;
- 玩家通过 Q 键控制退出游戏;
2)相机
- 主相机跟随:主相机始终在玩家后上方的位置,并保持与玩家的相对位置不变;
- 次相机实现小地图:次相机俯拍战场,并将影像在屏幕右上角显示;
- 单击小地图,小地图全屏,再单击,小地图恢复,在全屏和恢复的过程中有缩放动效。
3)坦克属性
- 坦克属性:血量、移动速度、旋转速度、颜色、初始位置、初始方位。
4)炮弹属性
- 炮弹属性:伤害、飞行速度、冷却时间、射程、颜色。
5)敌方坦克生成策略
- 敌方坦克根据能力值分三个级别,对应比例为 3:2:1,颜色分别为灰、浅蓝、黄;
- 敌方坦克初始生成 10 个,之后每隔 2 秒生成一个,总共 50 个坦克;
- 敌方坦克生成位置和朝向都是随机的,并且能够保证坦克位置不会重叠。
6)敌方坦克作战策略
- 转向再开炮:玩家在敌方坦克 0.5 倍射程范围内,敌方坦克转向玩家,再向玩家开炮;
- 转向再靠近并开炮:玩家在敌方坦克 1 倍射程范围内,敌方坦克转向玩家,再向玩家移动,同时向玩家开炮
- 转向再靠近:玩家在敌方坦克 1.5 倍射程范围内,敌方坦克转向玩家,再向玩家移动;
- 随机巡逻:玩家在敌方坦克在 1.5 倍射程范围外,敌方坦克随机巡逻:将其 10 ~ 30米外的随机一处位置作为目标位置,转向目标位置,再向目标位置移动;如果到中途满足以上条件,则执行相应操作;如果不满足,继续向目标位置移动;如果中途发现前方 5 米范围内有友军,重新换一个目标位置继续巡逻;如果达到目标位置仍不满足以上条件,再换一个目标位置继续巡逻。
7)血条
- 玩家血条显示在屏幕左上角,玩家被命中时,扣除相应血量,血量进度条向左滑动,血量比例更新;
- 敌方坦克血条显示在坦克的上方,随坦克一起运动,坦克被命中时,扣除相应血量,血量进度条向左滑动;
- 敌方坦克在运动时,其血条始终朝向主相机。
8)杀敌计数
- 在屏幕左上角、玩家血条下方显示杀敌计数,每杀掉一个敌人,杀敌计数加 1,进度条向右滑动。
9)摇杆
- 摇杆显示在屏幕左下角,拖拽摇杆可以控制己方坦克平移;
- ↑ ↓ ← → 键(或 W、S、A、D 键)除了能控制己方坦克平移,还能控制摇杆移动。
10)死亡策略
- 坦克在死亡时,停止运动并停止发射炮弹,渐变为透明;
- 己方坦克死亡时,生成一个空对象替换己方坦克,使得玩家可以通过 ↑ ↓ ← → 键(或 W、S、A、D 键)及鼠标右键,继续查看战场。
2 相关技术栈
- Transform组件
- 人机交互
- 刚体组件Rigidbody
- 碰撞体组件Collider
- 物理射线
- 相机跟随
- Canvas渲染模式与锚点
- UGUI之Text
- UGUI之Image
- 血条
- UGUI回调函数
- 摇杆
- 退出全屏、退出游戏
- 相机
3 游戏对象
1)游戏界面
2)游戏对象层级结构
Hierarchy 窗口游戏对象:
预设体游戏对象:
3)Transform组件参数
1. Tank Transform 组件参数
Name | Type | Position | Rotation | Scale |
---|---|---|---|---|
Tank | Empty | (0, 0.25, 0) | (0, 0, 0) | (1, 1, 1) |
Botton | Cube | (0, 0, 0) | (0, 0, 0) | (2, 0.5, 2) |
Top | Cube | (0, 0.5, 0) | (0, 0, 0) | (1, 0.5, 1) |
Gun | Cylinder | (0, 0, 1.5) | (90, 0, 0) | (0.2, 1, 0.4) |
FirePoint | Empty | (0, 1.15, 0) | (0, 0, 0) | (1, 1, 1) |
补充:Tank 游戏对象添加了刚体组件,并修改 Mass = 100,Drag = 1,AngularDrag = 0.5,Tank 作为预设体拖拽到 Assets/Resources/Prefabs 目录下,敌方坦克和己方坦克共用 Tank 预设体。
2. 玩家 Blood RectTransform 组件参数
Name | Type | Pos | Width/Height | Anchors | Pivot | Color/Texture |
---|---|---|---|---|---|---|
Blood | Empty | (25, -30, 0) | (0, 0) | (0, 1), (0, 1) | (0.5, 0.5) | —— |
Progress | Empty | (45, 0, 0) | (0, 0) | (0, 0.5), (0, 0.5) | (0, 0.5) | —— |
Background | Image | (0, 0, 0) | (200, 20) | (0, 0.5), (0, 0.5) | (0, 0.5) | #FFFFFFFF |
Value | Image | (0, 0, 0) | (200, 20) | (0, 0.5), (0, 0.5) | (0, 0.5) | #F63C3CFF |
Rate | Text | (5, 0, 0) | (60, 30) | (0, 0.5), (0, 0.5) | (0, 0.5) | —— |
Description | Text | (0, 0, 0) | (50, 20) | (0, 0.5), (0, 0.5) | (0, 0.5) | —— |
补充:Value 的 ImageType 设置为 Filled,Fill Method 设置为 Horizontal,Fill Origin 设置为 Left。
3. Kill RectTransform 组件参数
Name | Type | Pos | Width/Height | Anchors | Pivot | Color/Texture |
---|---|---|---|---|---|---|
Kill | Empty | (25, -60, 0) | (0, 0) | (0, 1), (0, 1) | (0.5, 0.5) | —— |
Progress | Empty | (45, 0, 0) | (0, 0) | (0, 0.5), (0, 0.5) | (0, 0.5) | —— |
Background | Image | (0, 0, 0) | (200, 20) | (0, 0.5), (0, 0.5) | (0, 0.5) | #FFFFFFFF |
Value | Image | (0, 0, 0) | (200, 20) | (0, 0.5), (0, 0.5) | (0, 0.5) | #3CB8F6FF |
Rate | Text | (5, 0, 0) | (60, 30) | (0, 0.5), (0, 0.5) | (0, 0.5) | —— |
Description | Text | (0, 0, 0) | (50, 20) | (0, 0.5), (0, 0.5) | (0, 0.5) | —— |
补充:Value 的 ImageType 设置为 Filled,Fill Method 设置为 Horizontal,Fill Origin 设置为 Left。
4. Stick RectTransform 组件参数
Name | Type | Pos | Width/Height | Anchors | Pivot | Sprite |
---|---|---|---|---|---|---|
Stick | Empty | (75, 70, 0) | (0, 0) | (0, 0), (0, 0) | (0.5, 0.5) | —— |
Background | Image | (0, 0, 0) | (100, 100) | (0.5, 0.5), (0.5, 0.5) | (0.5, 0.5) | stick_bg |
Ball | Image | (0, 0, 0) | (40, 40) | (0.5, 0.5), (0.5, 0.5) | (0.5, 0.5) | stick_ball |
5. 敌人 Blood RectTransform 组件参数
Name | Type | Pos | Width/Height | Color/Texture |
---|---|---|---|---|
Blood | Canvas | (0, 0.85, 0) | (2, 0.2) | —— |
Background | Image | (0, 0, 0) | (2, 0.2) | #FFFFFFFF |
Value | Image | (0, 0, 0) | (2, 0.2) | #F63C3CFF |
补充: 敌人 Blood 的 Canvas 渲染模式是 World Space,Value 的 ImageType 设置为 Filled,Fill Method 设置为 Horizontal,Fill Origin 设置为 Right。
6. Plane、Bullet、MinimapCamera Transform 组件参数
Name | Type | Position | Rotation | Scale | Color/Texture |
---|---|---|---|---|---|
Plane | Plane | (0, 0, 0) | (0, 0, 0) | (10, 10, 10) | GrassRockyAlbedo |
Bullet | Sphere | (0, 0.5, -5) | (0, 0, 0) | (0.3, 0.3, 0.3) | #228439FF |
MinimapCamera | Camera | (0, 12, 0) | (90, 0, 0) | (1, 1, 1) | —— |
补充:Bullet 作为预设体拖拽到 Assets/Resources/Prefabs 目录下,并且添加了刚体组件,己方坦克和敌方坦克使用的炮弹预设体都是 Bullet;MinimapCamera 是次相机,用于渲染小地图,其 Viewport Rect 为 (0.8, 0.7, 0.2, 0.3),Culling Mask 中去掉勾选 UI 图层(不渲染敌人血条)。
4 游戏框架
1)类图
2)代码目录
5 代码实现
BaseCreater.cs
using System;
using UnityEngine;
public class BaseCreater : MonoBehaviour {
protected GameObject tankPrefab; // 坦克预设体
protected Action dyingAction; // 死亡活动
protected void LoadPrefab() { // 加载预设体
tankPrefab = (GameObject) Resources.Load("Prefabs/Tank");
}
protected GameObject CreateTank(TankInfo info) { // 创建坦克
GameObject tank = Instantiate(tankPrefab, info.tankInitPosition, Quaternion.Euler(info.tankInitRotation));
tank.name = info.GetName();
UpdateColor(tank, info.tankColor);
return tank;
}
private void UpdateColor(GameObject tank, Color color) { // 更新坦克颜色
ForAllChildren(tank, obj => {
MeshRenderer renderer = obj.GetComponent<MeshRenderer>();
if (renderer != null) {
renderer.material.color = color;
}
});
}
private void ForAllChildren(GameObject obj, Action<GameObject> action) { // 每个子孙对象(包含自己)执行委托动作
if (obj == null) {
return;
}
foreach(Transform child in obj.GetComponentsInChildren<Transform>()) {
action(child.gameObject);
}
}
}
PlayerCreater.cs
using System;
using UnityEngine;
using UnityEngine.UI;
public class PlayerCreater : BaseCreater {
[Header("坦克信息")]
public TankInfo tankInfo;
private void Awake() {
LoadPrefab();
tankInfo = TankParamsManager.GetInstance().GetPlayerTankInfo();
GameObject tank = CreateTank(tankInfo);
dyingAction += () => {
Camera.main.GetComponent<CameraController>().PlayerDying(); // 玩家死亡, 相机重新跟随一个空对象
};
tank.AddComponent<PlayerController>()
.SetDiedAction(dyingAction)
.SetTankInfo(tankInfo);
}
}
说明:PlayerCreater 脚本组件挂在 PlayerCreater 游戏对象上。
EnemyCreater.cs
using System;
using UnityEngine;
public class EnemyCreater : BaseCreater {
protected GameObject bloodPrefab; // 血条预设体
public EnemiesInfo enemiesInfo = null;
private Transform player; // 玩家
private KillHelper killHelper; // 杀敌助手
private float waitTime = 0; // 距离上次坦克生成已等待的时间
private void Awake() {
LoadPrefab();
enemiesInfo = new EnemiesInfo();
killHelper = new KillHelper(enemiesInfo.GetTotalNum());
dyingAction += () => {
enemiesInfo.CurrDes();
killHelper.KillEnemy();
};
}
private void Start() {
player = GameObject.Find("Player/Top").transform;
CreateInitEnemies();
}
private void Update() {
if (waitTime > EnemiesInfo.ENTER_INTERVAL && enemiesInfo.GetAppeardNum() < EnemiesInfo.TOTAL_NUM) {
CreateTank();
waitTime = 0;
}
waitTime += Time.deltaTime;
}
protected new void LoadPrefab() { // 加载预设体
base.LoadPrefab();
bloodPrefab = (GameObject) Resources.Load("Prefabs/Blood");
}
private void CreateInitEnemies() {
for (int i = 0; i < EnemiesInfo.INIT_NUM; i++) {
CreateTank();
}
}
private GameObject CreateTank() { // 创建坦克
int level = enemiesInfo.GetRandomLevel();
TankInfo tankInfo = TankParamsManager.GetInstance().GetEnemyTankInfo(level);
Vector3 position = GetEnemyPosition();
Vector3 rotation = GetEnemyRotation();
tankInfo.tankInitPosition = position;
tankInfo.tankInitRotation = rotation;
GameObject tank = CreateTank(tankInfo);
CreateHealth(tank);
tank.AddComponent<EnemyController>()
.SetPlayer(player)
.SetDiedAction(dyingAction)
.SetTankInfo(tankInfo);
enemiesInfo.CurrAdd();
return tank;
}
private Vector3 GetEnemyPosition() { // 获取敌人随机位置
Vector3 position = new Vector3(0f, 0.25f, 0f);
do {
position.x = UnityEngine.Random.Range(-49f, 49f);
position.z = UnityEngine.Random.Range(-49f, 49f);
} while (IsOccupied(position));
return position;
}
private Vector3 GetEnemyRotation() { // 获取敌人随机方向
int y = UnityEngine.Random.Range(0, 360);
return new Vector3(0, y, 0);
}
private bool IsOccupied(Vector3 pos) { // 检测指定位置是否被占用
Vector3 halfExtents = new Vector3(2, 0.1f, 2);
return Physics.CheckBox(pos, halfExtents);
}
private void CreateHealth(GameObject tank) { // 创建血条
Vector3 pos = tank.transform.position;
Vector3 realPos = new Vector3(pos.x, pos.y + 1.5f, pos.z);
GameObject blood = Instantiate(bloodPrefab, realPos, Quaternion.identity, tank.transform);
blood.name = "Blood";
}
}
说明:EnemyCreater 脚本组件挂在 EnemyCreater 游戏对象上。
TankController.cs
using System;
using UnityEngine;
using UnityEngine.UI;
public class TankController : MonoBehaviour {
protected TankInfo tankInfo; // 坦克信息
protected GameObject bulletPrefab; // 炮弹预设体
protected Transform firePoint; // 开火点
protected BloodHelper bloodHelper; // 血量助手
protected float fireWaitTime = float.MaxValue; // 距离上次开火已等待的时间
protected volatile bool isDying = false; // 死亡中
protected Action diedAction; // 敌人死亡活动
protected void LoadBullet() {
bulletPrefab = (GameObject) Resources.Load("Prefabs/Bullet");
firePoint = transform.Find("Top/Gun/FirePoint");
}
public TankController SetTankInfo(TankInfo info) {
tankInfo = info;
return this;
}
public TankController SetDiedAction(Action action) {
diedAction = action;
return this;
}
protected void Fire() { // 开炮
if (fireWaitTime > tankInfo.bullet.bulletCoolTime) {
BulletInfo info = tankInfo.bullet.Clone() as BulletInfo;
info.SetFlyDir(transform.forward);
GameObject bullet = Instantiate(bulletPrefab, firePoint.position, Quaternion.identity);
bullet.AddComponent<BulletController>().SetBulletInfo(info);
fireWaitTime = 0f;
}
}
public void BuckleBlood(int damage) { // 扣血
isDying = bloodHelper.BuckleBlood(damage);
}
public bool IsDying() {
return isDying;
}
protected void Dying() { // 坦克死亡中, 渐变为透明
ForAllChildren(gameObject, obj => {
Renderer renderer = obj.GetComponent<Renderer>();
if (renderer != null) {
Color color = renderer.material.color;
// 材质球的 Rendering Mode 需要设置为 Fade 或 Transparent, 渐变才会生效
renderer.material.color = new Color(color.r, color.g, color.b, color.a - Time.deltaTime / 3);
}
Image image = obj.GetComponent<Image>();
if (image != null) {
Color color = image.color;
image.color = new Color(color.r, color.g, color.b, color.a - Time.deltaTime / 3);
}
});
}
protected bool CheckFallDeath() { // 检验摔死
if (!isDying && transform.position.y < -5) {
isDying = true;
Destroy(gameObject, 3);
diedAction.Invoke();
return true;
}
return false;
}
protected void ForAllChildren(GameObject obj, Action<GameObject> action) { // 每个子孙对象(包含自己)执行委托动作
if (obj == null) {
return;
}
foreach(Transform child in obj.GetComponentsInChildren<Transform>()) {
action(child.gameObject);
}
}
}
PlayerController.cs
using System;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : TankController {
private MoveHelper moveHelper; // 交互助手
private StickController stick; // 摇杆控制器
private void Awake() {
LoadBullet();
stick = GameObject.Find("UI/Stick/Ball").GetComponent<StickController>();
moveHelper = new MoveHelper(transform);
}
private void Start() {
diedAction += () => {
stick.PlayerDying();
};
moveHelper.Init(tankInfo.tankMoveSpeed, tankInfo.tankRotateSpeed);
LoadBlood();
}
private void Update() {
if (isDying || CheckFallDeath()) {
Dying();
return;
}
fireWaitTime += Time.deltaTime;
moveHelper.Move();
moveHelper.Rotate();
if (Input.GetMouseButtonDown(0) && !stick.IsMouseInStickArea()
|| Input.GetKeyDown(KeyCode.Space)) {
Fire();
}
}
private void LoadBlood() {
Image bloodValueImg = GameObject.Find("UI/Blood/Progress/Value").GetComponent<Image>();
Text bloodRateTxt = GameObject.Find("UI/Blood/Progress/Rate").GetComponent<Text>();
bloodHelper = new BloodHelper(bloodValueImg, bloodRateTxt, tankInfo, diedAction);
}
public void Move(float hor, float ver) { // 坦克移动
moveHelper.Move(hor, ver);
}
}
说明:PlayerController 脚本组件挂在 Player 游戏对象上。
EnemyController.cs
using System;
using UnityEngine;
using UnityEngine.UI;
public class EnemyController : TankController {
private Transform player; // 玩家
private Transform top; // 炮头
private Vector3 tempTarget; // 临时目标点,巡逻时使用
private bool isFoundPlayer = false; // 是否发现了玩家
private float findTargetTime = 0; // 巡逻时寻找目标花费的时间
private void Awake() {
LoadBullet();
top = transform.Find("Top");
Vector3 pos = top.position;
tempTarget = new Vector3(pos.x, pos.y, pos.z);
}
private void Start(){
LoadBlood();
}
private void Update() {
if (isDying || CheckFallDeath()) {
Dying();
return;
}
bloodHelper.BloodLookAtCamera();
float distance = GetDistance();
if (distance < 1.5f * tankInfo.bullet.bulletFireRange) { // 发现了玩家
if (distance < tankInfo.bullet.bulletFireRange / 2 || distance < 5) {
Fire();
} else if (distance < tankInfo.bullet.bulletFireRange) {
MoveToTarget(player.position);
Fire();
} else {
MoveToTarget(player.position);
}
isFoundPlayer = true;
findTargetTime = 0;
} else {
Patrol();
isFoundPlayer = false;
}
fireWaitTime += Time.deltaTime;
}
public EnemyController SetPlayer(Transform transform) {
player = transform;
return this;
}
protected new void Fire() { // 开炮
if (LookAtTarget(player.position)) {
base.Fire();
}
}
private bool LookAtTarget(Vector3 target) { // 转向目标
Vector3 dir = target - top.position;
float angle = Vector3.Angle(dir, top.forward);
if (angle > 5) {
int axis = Vector3.Dot(Vector3.Cross(dir, top.forward), Vector3.up) > 0 ? -1 : 1;
GetComponent<Rigidbody>().angularVelocity = axis * Vector3.up * tankInfo.tankRotateSpeed;
return false;
}
GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
return true;
}
private void MoveToTarget(Vector3 target) { // 向目标移动
if (LookAtTarget(target)) {
Vector3 dir = (target - top.position).normalized;
GetComponent<Rigidbody>().velocity = tankInfo.tankMoveSpeed * dir;
findTargetTime += Time.deltaTime;
}
}
private void Patrol() { // 巡逻
GetNextPatrolTarget();
MoveToTarget(tempTarget);
}
private float GetDistance() { // 获取坦克距离玩家的距离
if (player == null) {
return float.MaxValue;
}
return Vector3.Distance(player.position, top.position);
}
private void GetNextPatrolTarget() { // 获取下一个巡逻目标点
if (isFoundPlayer || Vector3.Distance(tempTarget, top.position) < 0.1f
|| ForwardHasFriends() && findTargetTime > 1) {
float angle = top.eulerAngles.y + UnityEngine.Random.Range(-120f, 120f);
angle = angle * Mathf.PI / 180f;
float radius = UnityEngine.Random.Range(10f, 30f);
tempTarget.x = Mathf.Clamp(top.position.x + radius * Mathf.Cos(angle), -49, 49);
tempTarget.z = Mathf.Clamp(top.position.z + radius * Mathf.Sin(angle), -49, 49);
findTargetTime = 0;
}
}
private bool ForwardHasFriends() { // 前方有友军
return Physics.BoxCast(top.position, new Vector3(1, 0.5f, 1), top.forward, top.rotation, 5, ~(1 << 8));
}
private void LoadBlood() {
Transform blood = transform.Find("Blood");
Image bloodValueImg = blood.Find("Value").GetComponent<Image>();
bloodHelper = new BloodHelper(bloodValueImg, null, tankInfo, diedAction);
bloodHelper.setTransform(blood);
}
}
说明:EnemyController 脚本组件挂在 Enemy 游戏对象上。
BulletController.cs
using UnityEngine;
public class BulletController : MonoBehaviour {
private BulletInfo bulletInfo; // 炮弹信息
private bool isDying = false;
private void Start () {
GetComponent<MeshRenderer>().material.color = bulletInfo.bulletColor;
float lifeTime = bulletInfo.bulletFireRange / bulletInfo.bulletSpeed; // 存活时间
Destroy(gameObject, lifeTime);
}
private void Update () {
transform.GetComponent<Rigidbody>().velocity = bulletInfo.GetFlyDir() * bulletInfo.bulletSpeed;
}
private void OnCollisionEnter(Collision other) {
TankController tankController = other.gameObject.GetComponent<TankController>();
if (isDying || tankController == null || tankController.IsDying()) {
return;
}
if (isHitEnemy(bulletInfo.GetName(), other.gameObject.name)
|| isHitPlayer(bulletInfo.GetName(), other.gameObject.name)) {
tankController.BuckleBlood(bulletInfo.bulletDamage);
if (tankController.IsDying()) {
Destroy(other.gameObject, 3f);
}
}
isDying = true;
Destroy(gameObject, 0.3f);
}
public void SetBulletInfo(BulletInfo info) {
bulletInfo = info;
}
private bool isHitEnemy(string bulletName, string roleName) { // 击中敌军
return "PlayerBullet".Equals(bulletName) && "Enemy".Equals(roleName);
}
private bool isHitPlayer(string bulletName, string roleName) { // 击中玩家
return "EnemyBullet".Equals(bulletName) && "Player".Equals(roleName);
}
}
说明:BulletController 脚本组件挂在 Bullet 游戏对象上。
CameraController.cs
using UnityEngine;
public class CameraController : MonoBehaviour {
protected Transform player; // 玩家
protected Transform visitor; // 游客
protected Vector3 relaPlayerPos; // 相机在玩家坐标系中的位置
protected void InitPlayer() {
GameObject obj = GameObject.Find("Player/Top");
if (obj != null) {
player = obj.transform;
CompCameraPos(player);
}
}
public void PlayerDying() { // 玩家死亡时, 游客替换玩家的位置, 使相机跟随游客
GameObject visitorObj = new GameObject("Visitor");
visitorObj.AddComponent<VisitorController>();
visitor = visitorObj.transform;
visitor.position = player.position;
visitor.rotation = player.rotation;
player = null;
}
protected Transform getVisitor() { // 给小地图相机使用
GameObject visitorObj = GameObject.Find("Visitor");
if (visitorObj != null) {
return visitorObj.transform;
}
return visitor;
}
// 求以origin为原点, locX, locY, locZ 为坐标轴的本地坐标系中的向量 vec 在世界坐标系中对应的向量
protected Vector3 transformVecter(Vector3 vec, Vector3 origin, Vector3 locX, Vector3 locY, Vector3 locZ) {
return vec.x * locX + vec.y * locY + vec.z * locZ + origin;
}
protected void CompCameraPos(Transform target) { // 计算相机位置
}
}
MainCameraController.cs
using UnityEngine;
public class MainCameraController : CameraController {
private float targetDistance = 30f; // 相机看向玩家前方的位置
private void Start() {
relaPlayerPos = new Vector3(0, 8, -12);
InitPlayer();
}
private void LateUpdate() {
if (player != null) {
CompCameraPos(player);
} else if (visitor == null) {
visitor = getVisitor();
} else {
CompCameraPos(visitor);
}
}
protected new void CompCameraPos(Transform target) { // 计算相机坐标
transform.position = transformVecter(relaPlayerPos, target.position, target.right, target.up, target.forward);
if (transform.position.y < 0) { // 避免坦克翻面时相机朝天
transform.position = new Vector3(transform.position.x, -transform.position.y, transform.position.z);
}
Vector3 lookPos = target.position + target.forward * targetDistance;
if (lookPos .y > 1) { // 避免坦克扬起时相机朝天
lookPos = new Vector3(lookPos.x, 1, lookPos.z);
}
transform.rotation = Quaternion.LookRotation(lookPos - transform.position);
}
}
说明:MainCameraController 脚本组件挂在 MainCamera 游戏对象上。
MinimapCameraController.cs
using UnityEngine;
public class MinimapCameraController : CameraController {
private CameraController mainCamera;
private bool isFullscreen = false; // 小地图相机是否全屏
private Rect minViewport; // 小地图视口位置和大小(相对值)
private Rect fullViewport; // 全屏时视口位置和大小(相对值)
private Rect targetViewport; // 目标视口大小, 用于动效
private float switchRectInternal = 0.3f; // 切换小地图尺寸时间间隔
private float switchWaitTime = float.MaxValue; // 切换小地图等待时间
private bool isSwitching = false; // 切换中, 用于标记执行切换动效中
private void Start() {
mainCamera = Camera.main.GetComponent<CameraController>();
minViewport = GetComponent<Camera>().rect;
fullViewport = new Rect(0, 0, 1, 1);
relaPlayerPos = new Vector3(0, 50, 0);
InitPlayer();
}
private void Update() {
switchWaitTime += Time.deltaTime;
if (switchWaitTime > switchRectInternal && !isSwitching) {
SwitchRect();
}
if (isSwitching) {
SwitchAnimation();
}
}
private void LateUpdate() {
if (player != null) {
CompCameraPos(player);
} else if (visitor == null) {
visitor = getVisitor();
} else {
CompCameraPos(visitor);
}
}
private void SwitchRect() { // 切换小地图尺寸
if (Input.GetMouseButtonDown(0) && IsClickMinimap()) {
if (isFullscreen) { // 小地图缩小
targetViewport = minViewport;
} else { // 小地图放大
targetViewport = fullViewport;
}
isFullscreen = !isFullscreen;
isSwitching = true;
switchWaitTime = 0;
}
}
private void SwitchAnimation() { // 切换动效
Rect rect = GetComponent<Camera>().rect;
rect.position = Vector2.Lerp(rect.position, targetViewport.position, 0.5f);
rect.size = Vector2.Lerp(rect.size, targetViewport.size, 0.5f);
GetComponent<Camera>().rect = rect;
if (Vector2.Distance(rect.position, targetViewport.position) < 0.01f) {
GetComponent<Camera>().rect = targetViewport;
isSwitching = false;
}
}
protected new void CompCameraPos(Transform target) { // 计算相机坐标
transform.position = target.position + relaPlayerPos;
}
public bool IsClickMinimap() { // 是否单击到小地图区域
Vector3 pos = Input.mousePosition;
if (isFullscreen || isSwitching) {
return true;
}
if (pos.x / Screen.width > minViewport.xMin && pos.y / Screen.height > minViewport.yMin) {
return true;
}
return false;
}
}
说明:MinimapCameraController 脚本组件挂在 MinimapCamera 游戏对象上。
VisitorController.cs
using UnityEngine;
public class VisitorController : MonoBehaviour {
private MoveHelper moveHelper; // 交互助手
private void Awake() {
moveHelper = new MoveHelper(transform);
moveHelper.Init(6, 10);
}
private void Update() {
moveHelper.Move();
moveHelper.Rotate();
}
}
说明:VisitorController 脚本组件挂在 Visitor 游戏对象上。
StickController.cs
using UnityEngine;
using UnityEngine.EventSystems;
public class StickController : MonoBehaviour, IDragHandler, IEndDragHandler {
private Vector3 originPos; // 鼠标开始拖拽时位置
private Vector3 currPos; // 鼠标当前位置
private float radius; // 遥杆半径
private PlayerController player; // 玩家控制器
private Vector3 dire = Vector3.zero; // 摇杆球的方位
private void Start () {
originPos = transform.position;
radius = 50;
player = GameObject.Find("Player").GetComponent<PlayerController>();
}
private void Update () {
if (player != null && !Vector3.zero.Equals(dire)) {
player.Move(dire.x, dire.y);
}
}
public void OnDrag(PointerEventData eventData) {
Vector3 vec = Input.mousePosition - originPos;
dire = vec.normalized * Mathf.Min(vec.magnitude / radius, 1);
UpdateStick(dire);
}
public void OnEndDrag(PointerEventData eventData) {
transform.position = originPos;
dire = Vector3.zero;
}
public void UpdateStick(Vector3 dire) { // 更新摇杆位置
transform.position = originPos + dire * radius;
}
public void PlayerDying() {
player = null;
UpdateStick(Vector3.zero);
}
public bool IsMouseInStickArea() { // 鼠标在Stick区域
Vector3 pos = Input.mousePosition;
return pos.x < 150 && pos.y < 150;
}
}
说明:StickController 脚本组件挂在 Stick 游戏对象上。
BloodHelper.cs
using System;
using UnityEngine;
using UnityEngine.UI;
public class BloodHelper {
private Transform transform; // 血条Transform
private Image bloodValueImg; // 血条Image
private Text bloodRateTxt; // 血量比率Text
private TankInfo tankInfo; // 坦克信息
private Action diedAction; // 死亡活动
public BloodHelper(Image bloodValueImg, Text bloodRateTxt, TankInfo tankInfo, Action diedAction) {
this.bloodValueImg = bloodValueImg;
this.bloodRateTxt = bloodRateTxt;
this.tankInfo = tankInfo;
this.diedAction = diedAction;
bloodValueImg.fillAmount = 1;
if (bloodRateTxt != null) {
bloodRateTxt.text = "" + tankInfo.GetFullBlood() + " / " + tankInfo.GetFullBlood();
}
}
public bool BuckleBlood(int damage) { // 扣血
bloodValueImg.fillAmount = tankInfo.BuckleBlood(damage);
if (bloodRateTxt != null) {
bloodRateTxt.text = "" + tankInfo.GetCurrBlood() + " / " + tankInfo.GetFullBlood();
}
bool isDying = tankInfo.IsDying();
if (isDying) {
diedAction.Invoke();
}
return isDying;
}
public void setTransform(Transform transform) {
this.transform = transform;
}
public void BloodLookAtCamera() { // 血条朝向相机
Vector3 cameraPos = Camera.main.transform.position;
Vector3 target = new Vector3(cameraPos.x, transform.position.y, cameraPos.z);
transform.LookAt(target);
}
}
KillHelper.cs
using UnityEngine;
using UnityEngine.UI;
public class KillHelper {
private Image killValueImg; // 杀敌Image
private Text killRateTxt; // 杀敌比率Text
private int enemiesNum; // 敌人总数
private int currKillEnemies; // 当前杀敌数
public KillHelper(int enemiesNum) {
this.enemiesNum = enemiesNum;
currKillEnemies = 0;
killValueImg = GameObject.Find("UI/Kill/Progress/Value").GetComponent<Image>();
killRateTxt = GameObject.Find("UI/Kill/Progress/Rate").GetComponent<Text>();
UpdateUI();
}
public void KillEnemy() { // 杀敌
lock(this) {
currKillEnemies++;
UpdateUI();
}
}
private void UpdateUI() {
killValueImg.fillAmount = 1.0f * currKillEnemies / enemiesNum;
killRateTxt.text = "" + currKillEnemies + " / " + enemiesNum;
}
}
MoveHelper.cs
using UnityEngine;
public class MoveHelper {
private Transform transform; // 交互对象的变换组件
protected StickController stick; // 摇杆控制器
private Vector3 predownMousePoint; // 鼠标右键按下时的位置
private Vector3 currMousePoint; // 鼠标右键滑动过程中的位置
private float moveSpeed; // 移动速度
private float rotateSpeed; // 旋转速度
public MoveHelper(Transform transform) {
this.transform = transform;
stick = GameObject.Find("UI/Stick/Ball").GetComponent<StickController>();
}
public void Init(float moveSpeed, float rotateSpeed) {
this.moveSpeed = moveSpeed;
this.rotateSpeed = rotateSpeed;
}
public void Move() { // 移动
float hor = Input.GetAxis("Horizontal");
float ver = Input.GetAxis("Vertical");
Move(hor, ver);
if (Mathf.Abs(hor) > float.Epsilon || Mathf.Abs(ver) > float.Epsilon) {
updateStick(hor, ver);
}
}
public void Move(float hor, float ver) { // 移动
if (Mathf.Abs(hor) > float.Epsilon || Mathf.Abs(ver) > float.Epsilon) {
Rigidbody rigidbody = transform.GetComponent<Rigidbody>();
if (rigidbody != null) {
Vector3 vec = transform.forward * ver + transform.right * hor;
rigidbody.velocity = vec * moveSpeed;
} else {
transform.Translate(new Vector3(hor, 0, ver) * Time.deltaTime * moveSpeed);
}
RestrictBoundary();
}
}
public void Rotate() { // 旋转
if (Input.GetMouseButtonDown(1)) {
predownMousePoint = Input.mousePosition;
} else if (Input.GetMouseButton(1)) {
currMousePoint = Input.mousePosition;
Vector3 vec = currMousePoint - predownMousePoint;
Rigidbody rigidbody = transform.GetComponent<Rigidbody>();
if (rigidbody != null) {
rigidbody.angularVelocity = Vector3.up * rotateSpeed * vec.x;
} else {
transform.Rotate(Vector3.up * vec.x * Time.deltaTime * rotateSpeed);
}
predownMousePoint = currMousePoint;
}
}
private void updateStick(float hor, float ver) { // 更新摇杆
Vector3 dire = new Vector3(hor, ver, 0);
dire = dire.normalized * Mathf.Min(dire.magnitude, 1);
stick.UpdateStick(dire);
}
private void RestrictBoundary() { // 限制边界
Vector3 pos = transform.position;
if (Mathf.Abs(pos.x) > 49) {
transform.position = new Vector3(Mathf.Sign(pos.x) * 49, pos.y, pos.z);
} else if (Mathf.Abs(pos.z) > 49) {
transform.position = new Vector3(pos.x, pos.y, Mathf.Sign(pos.z) * 49);
}
}
}
TankInfo.cs
using System;
using UnityEngine;
[Serializable]
public class TankInfo : ICloneable {
[Header("坦克满血量")]
[Range(30, 100)]
public int fullBlood = 30;
[Header("坦克移动速度")]
[Range(1, 5)]
public float tankMoveSpeed = 3f;
[Header("坦克旋转速度")]
[Range(1, 4)]
public float tankRotateSpeed = 2f;
[Header("坦克颜色")]
public Color tankColor = Color.green;
[Header("坦克初始位置")]
public Vector3 tankInitPosition = new Vector3(0f, 0.25f, 0f);
[Header("坦克初始方位")]
public Vector3 tankInitRotation = new Vector3(0, 0, 0);
[Header("炮弹信息")]
public BulletInfo bullet;
private String name = "Enemy"; // 坦克角色名
private int currBlood = 30; // 坦克当前血量
public TankInfo(String name) {
this.name = name;
bullet = new BulletInfo(name + "Bullet");
}
public object Clone() { // 克隆坦克
TankInfo info = new TankInfo(this.name);
info.fullBlood = this.fullBlood;
info.tankMoveSpeed = this.tankMoveSpeed;
info.tankRotateSpeed = this.tankRotateSpeed;
info.tankColor = this.tankColor;
info.tankInitPosition = this.tankInitPosition;
info.tankInitRotation = this.tankInitRotation;
info.bullet = this.bullet.Clone() as BulletInfo;
info.SetCurrBlood(this.currBlood);
return info;
}
public void SetCurrBlood(int blood) { // 设置当前血量
currBlood = blood;
}
public int GetCurrBlood() { // 获取当前血量
return currBlood;
}
public int GetFullBlood() { // 获取满血量
return fullBlood;
}
public float BuckleBlood(int damage) { // 扣血, 返回剩余血量比例
currBlood = Math.Max(currBlood - damage, 0);
return 1.0f * currBlood / fullBlood;
}
public bool IsDying() {
return currBlood == 0;
}
public String GetName() {
return name;
}
}
BulletInfo.cs
using System;
using UnityEngine;
[Serializable]
public class BulletInfo : ICloneable {
[Header("炮弹伤害")]
[Range(2, 10)]
public int bulletDamage = 2;
[Header("炮弹飞行速度")]
[Range(5, 20)]
public int bulletSpeed = 10;
[Header("炮弹冷却时间")]
[Range(0.1f, 1.5f)]
public float bulletCoolTime = 0.3f;
[Header("炮弹射程")]
[Range(5, 15)]
public float bulletFireRange = 10;
[Header("炮弹颜色")]
public Color bulletColor = Color.red;
private String name = "EnemyBullet"; // 炮弹名
private Vector3 flyDir; // 炮弹飞行方向
public BulletInfo(String name) {
this.name = name;
}
public void SetFlyDir(Vector3 flyDir) {
this.flyDir = flyDir;
}
public Vector3 GetFlyDir() {
return flyDir;
}
public String GetName() {
return name;
}
public object Clone() { // 克隆炮弹
BulletInfo info = new BulletInfo(this.name);
info.bulletDamage = this.bulletDamage;
info.bulletSpeed = this.bulletSpeed;
info.bulletCoolTime = this.bulletCoolTime;
info.bulletFireRange = this.bulletFireRange;
info.bulletColor = this.bulletColor;
return info;
}
}
EnemiesInfo.cs
using UnityEngine;
public class EnemiesInfo {
public const int TOTAL_NUM = 50; // 敌人总数
public const int INIT_NUM = 10; // 敌人初始入场个数
public const int ENTER_INTERVAL = 2; // 敌人入场时间间隔
public const int LEVEL_1_WEIGHT = 3; // 一级敌人个数权重
public const int LEVEL_2_WEIGHT = 2; // 二级敌人个数权重
public const int LEVEL_3_WEIGHT = 1; // 三级敌人个数权重
private volatile int appeardNum = 0; // 已入场的敌人数
private volatile int currentNum = 0; // 当前敌人数
private int totalWeight; // 总权重
public EnemiesInfo() {
totalWeight = LEVEL_1_WEIGHT + LEVEL_2_WEIGHT + LEVEL_3_WEIGHT;
}
public void CurrAdd() {
lock(this) {
appeardNum ++;
currentNum ++;
}
}
public void CurrDes() {
lock(this) {
currentNum --;
}
}
public int GetRandomLevel() {
float random = Random.Range(0f, totalWeight);
if (random < LEVEL_1_WEIGHT) {
return 1;
}
if (random < LEVEL_1_WEIGHT + LEVEL_2_WEIGHT) {
return 2;
}
return 3;
}
public int GetAppeardNum() {
return appeardNum;
}
public int GetTotalNum() {
return TOTAL_NUM;
}
}
TankParamsManager.cs
using UnityEngine;
public class TankParamsManager {
private const int MIN_ENEMY_LEVEL = 1;
private const int MAX_ENEMY_LEVEL = 3;
private static TankParamsManager tankParamsManager;
private TankInfo LEVEL_0 = new TankInfo("Player"); // 玩家
private TankInfo LEVEL_1 = new TankInfo("Enemy"); // 敌军1级
private TankInfo LEVEL_2 = new TankInfo("Enemy"); // 敌军2级
private TankInfo LEVEL_3 = new TankInfo("Enemy"); // 敌军3级
private TankParamsManager() {
Level0Params();
Level1Params();
Level2Params();
Level3Params();
}
public static TankParamsManager GetInstance() {
if (tankParamsManager == null) {
tankParamsManager = new TankParamsManager();
}
return tankParamsManager;
}
public TankInfo GetPlayerTankInfo() {
return LEVEL_0.Clone() as TankInfo;
}
public TankInfo GetEnemyTankInfo(int level) {
level = Camp(level, MIN_ENEMY_LEVEL, MAX_ENEMY_LEVEL);
switch (level) {
case 1:
return LEVEL_1.Clone() as TankInfo;
case 2:
return LEVEL_2.Clone() as TankInfo;
case 3:
return LEVEL_3.Clone() as TankInfo;
}
return LEVEL_1;
}
private void Level0Params() {
LEVEL_0.fullBlood = 500;
// LEVEL_0.fullBlood = 2;
LEVEL_0.tankColor = Color.green;
LEVEL_0.tankMoveSpeed = 4;
LEVEL_0.tankRotateSpeed = 2;
LEVEL_0.SetCurrBlood(LEVEL_0.fullBlood);
LEVEL_0.bullet.bulletDamage = 10;
LEVEL_0.bullet.bulletColor = Color.red;
LEVEL_0.bullet.bulletCoolTime = 0.15f;
LEVEL_0.bullet.bulletFireRange = 15;
LEVEL_0.bullet.bulletSpeed = 10;
}
private void Level1Params() {
LEVEL_1.fullBlood = 30;
LEVEL_1.tankColor = Color.gray;
LEVEL_1.tankMoveSpeed = 1;
LEVEL_1.tankRotateSpeed = 0.85f;
LEVEL_1.SetCurrBlood(LEVEL_1.fullBlood);
LEVEL_1.bullet.bulletDamage = 2;
LEVEL_1.bullet.bulletColor = Color.gray;
LEVEL_1.bullet.bulletCoolTime = 2f;
LEVEL_1.bullet.bulletFireRange = 10;
LEVEL_1.bullet.bulletSpeed = 5;
}
private void Level2Params() {
LEVEL_2.fullBlood = 40;
LEVEL_2.tankColor = Color.cyan;
LEVEL_2.tankMoveSpeed = 1.3f;
LEVEL_2.tankRotateSpeed = 0.9f;
LEVEL_2.SetCurrBlood(LEVEL_2.fullBlood);
LEVEL_2.bullet.bulletDamage = 3;
LEVEL_2.bullet.bulletColor = Color.gray;
LEVEL_2.bullet.bulletCoolTime = 1.7f;
LEVEL_2.bullet.bulletFireRange = 11;
LEVEL_2.bullet.bulletSpeed = 6;
}
private void Level3Params() {
LEVEL_3.fullBlood = 50;
LEVEL_3.tankColor = Color.yellow;
LEVEL_3.tankMoveSpeed = 1.6f;
LEVEL_3.tankRotateSpeed = 1f;
LEVEL_3.SetCurrBlood(LEVEL_3.fullBlood);
LEVEL_3.bullet.bulletDamage = 4;
LEVEL_3.bullet.bulletColor = Color.yellow;
LEVEL_3.bullet.bulletCoolTime = 1.4f;
LEVEL_3.bullet.bulletFireRange = 12;
LEVEL_3.bullet.bulletSpeed = 7;
}
private int Camp(int value, int min, int max) {
if (value > max) {
return max;
}
if (value < min) {
return min;
}
return value;
}
}
WindowController.cs
using UnityEngine;
public class WindowController : MonoBehaviour {
private bool fullscreen = true; // 全屏
private void Update () {
if (Input.GetKeyDown(KeyCode.Escape)) {
fullscreen = !fullscreen;
Screen.fullScreen = fullscreen;
}
if (Input.GetKeyDown(KeyCode.Q)) {
Application.Quit();
}
}
}
说明:WindowController 脚本组件挂在 Window 游戏对象上。
6 运行效果
声明:本文转自【Unity3D】Tank大战