游戏规则:
①玩家1和玩家2分别控制自己的坦克,保护己方坦克同时尝试去摧毁对方坦克,存活的一方胜利。
②玩家1 WASD控制移动,space开火;玩家2 上下左右控制移动,右enter开火。
③玩家们都不可以离开地图范围。
④地图中的建筑物会阻碍坦克的行动。
项目资源
BatallaCL/MyUnity at master (github.com)https://github.com/BatallaCL/MyUnity/tree/master
游戏截图
游戏架构
使用了MVC架构。
- 场景中的所有GameObject就是Model,它们来自于Models(已经建好的模型)、Prefabs(已经建好的prefab)、AudioClip(音频文件)等文件夹(下面也会在用到具体文件时在详述),并且都受到Script中文件的控制,比如说坦克的开火交互受到FireAttack的控制,MainCamera受到FollowPlayer的控制……
- 控制文件都放在了Script文件夹中
游戏搭建
场景设置
首先,在Prefabs中将LevelArt加入到Hierarchy中,可以在Scene处看到生成的LevelArt(制作好的关卡地图模型)。这里可以利用Models中的模型对关卡中的房屋树木等障碍物做任意自己喜欢的设置,也可以调整Directional Light或在Edit→ProjectSettings→Lighting中设置场景光亮、背景颜色等。
同样将一个Models中的Tank加入到Hierarchy中,这会在LevelArt上直接生成一个Tank,这里可以对Tank除高度外的坐标做任意调整。
区分玩家按键
因为笔者搭建的3D大战是双人游戏,所以为了区分玩家按键需要进行以下设置:在Edit→ProjectSettings→Input Manager中复制Horizontal和Vertical并修改名字和按键。
笔者修改后的设置如图:
Tank移动
注:可以将Scene中的Tank加入到Prefabs中,此后对Tank进行的任意修改都可以直接Overrides→Apply All应用到Prefabs Tank中,之后加入Tank(添加玩家时)则更加便捷。
Tank→Add Component→Box Collider(使得Tank模型能够与LevelArt中的对象碰撞,从而起到障碍物的作用)
Tank→Add Component→Rigidbody(用于设置前后移动,勾选Free Position Y、Free RotationX、Free Rotation Z,使得Tank只能在X、Z方向移动,只能绕Y轴旋转)
Tank→Add Component→Tank Movement(Script)
Tank Movement的代码如下:(这里用到了上面所区分的玩家按键,RigidBody根据按键对应的玩家,控制其Tank的移动和旋转)
// Tank Movement
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class tankMovement : MonoBehaviour
{
private Rigidbody rigidbody;
public float id = 1; // 玩家编号 区分控制
// Start is called before the first frame update
void Start()
{
rigidbody = this.GetComponent<Rigidbody>();
}
private void FixedUpdate()// 在固定帧调用 可放置位移操作
{
float v = Input.GetAxis("VerticalPlayer"+id); // 获取前后位置 -1到1对应向后到向前
rigidbody.velocity = transform.forward * v * 8; // 前后移动 速度为8单位每秒
float h = Input.GetAxis("HorizontalPlayer"+id); // 获取水平位置
rigidbody.angularVelocity = transform.up * h * 8; // 围绕y轴旋转8单位
}
}
完成好上述内容后,玩家就可以在运行后操控Tank进行移动和旋转了。
Tank开火
在Tank对象中添加一个empty对象,并更名为FirePosition用于Tank开火后Shell(子弹)的生成。
Tank→Add Component→Fire Attack(Script)
// FireAttack.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FireAttack : MonoBehaviour
{
public GameObject shell; // 用于应用子弹实例
public KeyCode FireKey = KeyCode.Space; // 发射键位设置 默认空格键
private Transform FirePosition; // 发射位置
public float shellspeed = 15;
// Start is called before the first frame update
void Start()
{
FirePosition = transform.Find("FirePosition"); // 获取发射位置 应用之前创建的empty对象
}
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(FireKey)) // 按下keycode时
{
GameObject go = GameObject.Instantiate(shell, FirePosition.position, FirePosition.rotation) as GameObject; // 实例化子弹到fireposition位置 旋转角度也保持一致
go.GetComponent<Rigidbody>().velocity = go.transform.forward * shellspeed; // 给予子弹向前发射的速度
}
}
}
子弹的生成、碰撞和销毁
将Shell加入到Prefabs中,做以下设置:
Shell→Add Component→Capsule Collider→勾选Is Trigger(用于子弹碰撞处理)
Shell→Add Component→Rigidbpdy(用于子弹移动)
Shell→Add Component→Shell(Script 子弹碰撞处理脚本文件)
Tank→Add Component→Add Tag(加标签Tank,用于子弹识别碰撞物体是否是Tank)
ShellExplosion(Prefabs中制作好的子弹销毁特效)→Add Component→ShellExplosionDestory(script,用于特效结束后销毁对象)
// Shell(Script)
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class shell : MonoBehaviour
{
public GameObject shellExplosionPrefab; // 用于调用子弹销毁特效
public float time; // 子弹销毁时间设置
// Start is called before the first frame update
void Start()
{
Destroy(this.gameObject, time);
}
// Update is called once per frame
void Update()
{
}
public void OnTriggerEnter(Collider collider) // 子弹碰撞事件 必须打开shell对象的碰撞检测(is trigger) 必须打开shellexplos的play on awake使其在实例化后自动播放
{
GameObject.Instantiate(shellExplosionPrefab, transform.position, transform.rotation); // 实例化子弹爆炸特性
//GameObject.Destroy(this.gameObject); // 销毁子弹对象 该版本会自动销毁 所以注销掉了
if(collider.tag == "Tank")
{
// 调用碰撞对象的TankDamage函数
collider.SendMessage("TankDamage",null,SendMessageOptions.DontRequireReceiver);
}
}
}
// ShellExplosionDestory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShellExplosionDestroy : MonoBehaviour
{
public float time; // 设置为该特效的播放时间1.5s
// Start is called before the first frame update
void Start()
{
Destroy(this.gameObject,time);
}
// Update is called once per frame
void Update()
{
}
}
玩家生命(TankHealth)
玩家生命(TankHealth)决定了Tank的销毁和游戏的胜负,Tank在受到子弹碰撞后需要扣除一定生命值,并在扣除的生命值达到一定水平后销毁该对象,而对方玩家胜利。
Tank→Add Component→TankHealth(script,用于玩家生命处理)
// TankHealth.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TankHealth : MonoBehaviour
{
public int hp = 100;
public GameObject tankExplosion;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (hp <= 0) // 当坦克受到致命伤后 触发tankExplosion特效(同样要记得勾选play on awake)
{
// 播放tankExplosion特效
GameObject.Instantiate(tankExplosion, transform.position + Vector3.up, transform.rotation);
GameObject.Destroy(this.gameObject);
}
}
void TankDamage()
{
if (hp <= 0) return;
hp -= Random.Range(10, 20);
}
}
添加第二个玩家
前文已经完成了第二个玩家按键、玩家Id等的设置,这里只需要再加入一个tank对象,修改TankMovement的number值为2区别操作键位 ,修改FireAttack文件的fire key为任意自己喜欢的键位即可。
现在两个坦克一模一样,不太还区分玩家所操作的对象。为了更好的区分两个Tank, 可以用material材质替换tank模型各个组件中的mesh renderer的颜色材质。
视角跟随
原有的Main Camera对象是静止的,没法随Tank移动而移动,这也意味着玩家随时可能会丢失视野。所以,游戏还需要设置视角跟随。
Main Camera→Add Component→ FollowPlayer(Script,用于视角跟随)
// FollowPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FollowPlayer : MonoBehaviour
{
public Transform player1;
public Transform player2;
private Vector3 offset;
private Camera camera;
// Start is called before the first frame update
void Start()
{
offset = transform.position - (player1.position + player2.position) / 2;
camera = this.GetComponent<Camera>();
}
// Update is called once per frame
void Update()
{
if (player1 == null || player2 == null) return;// 有游戏玩家死亡则停止跟随
transform.position = (player1.position + player2.position) / 2 + offset;
float distance = Vector3.Distance(player1.position, player2.position);
float size = distance * 0.58f;
camera.orthographicSize = size;
}
}
那么,现在3D坦克大战的核心功能已经完成了,玩家可以操作两个Tank进行对战角逐胜利。
背景音乐
为了给游戏添加一点激情,简单添加了一个背景音乐,具体步骤如下:
添加empty对象(更名为GameManager)→Add Component→Audio Source→AudioClip替换为BackGroundMusic(AudioClips文件夹中)→勾选paly on awake和loop
现在运行游戏就可以听到derderder的背景音乐了。
可以做的扩展:
- 添加游戏分出胜负后,用户能够直接Restart的设置。
- 制作出更多地图,并且关卡化。
- 在地图中随机投放不同增益效果的道具,增强游戏的多样性和趣味性。