本案例是初级案例,意在引导想使用unity的初级开发者能较快的入门,体验unity开发的方便性和简易性能。
本次我们将使用团结引擎进行开发,帮助想体验团结引擎的入门开发者进行较快的环境熟悉。
本游戏是一个俯视角度的射击游戏。主角始终位于屏幕中心位置,玩家使用键盘控制主角移动,并可开枪射击场景中的敌人。玩家具有多种武器,如手枪、霰弹枪和自动步枪,玩家可以切换武器。敌人也会向玩家方向移动并射击玩家。当玩家角色或敌人的生命值减为零时则死亡。当玩家死亡后,游戏结束。
操作系统:Windows
Unity 版本:团结引擎1.0.0
团结引擎的安装参考《团结引擎的安装》
使用团结引擎新建项目和使用Unity Hub一样。打开Tuanjie Hub,点击左上角【create project】按钮,选择【3D】模版,输入项目名称和项目保存路径,点击右下【Create project】按钮完成创建。
1. 我们创建一个plane作为地面 。在Hierarchy面板上依次【右键】-> 【3D Object】->【plane】,将【Plane】创命名为【Ground】,并将x、z缩放大小变为4。我们将地面颜色改成灰色。新建文件夹,命名为【Materals】用来存放材质。在材质文件夹右键,依次选择【Create】->【Material】, 并命名为【Ground】。修改材质颜色为灰色(颜色值:4B4747),并将材质拖拽到地面上。
2. 同样, 我们创建一个胶囊体(Capsule)来作为主角,并命名为Player。创建一个立方体(Cube)放置在Player上,并调整到合适的位置。Cube将作为Player的正面,以便分辨方向。创建一个材质,命名为Face,改变材质样色为蓝色(也可以设置成自己喜欢的颜色),然后把这个材质赋值给Cube。
3. 调整摄像机角度
由于我们是俯视角度的射击游戏,因此需要调整摄像机的位置。选中摄像机,将摄像机调整到Player后方合适的位置。
4. 创建敌人
敌人角色与玩家角色造型基本一致,只是将白色换成红色。我们复制场景中的Player(选中Player, 按 ctrl + D),命名为Enermy。然后创建一个材质Enermy, 然后将材质的颜色改为红色。把材质赋给场景中的Enermy物体。新建标签(Tag),命名为Enermy,将玩家Player的Tag设置为“Player”, Enermy 的Tag设置为“Enermy”,方便区分玩家和敌人。之后我们新建文件夹,命名为 Prefabs,将Enermy制作成预知体(直接把Hierarchy中的Enermy物体拖到Prefabs文件夹中)。
上面基本场景已经搭建完毕,接下来我们就开始编写脚本,实现我们的游戏功能。
首先,我们来编写玩家脚本,让玩家动起来。新建文件夹,命名为【Scripts】,用来保存我自己编写的脚本文件。在【Scripts】右键->【Create】->【C# Script】,命名为Player。然后将Player脚本挂载到Player物体上。
和《3D小球跑酷》类似,我们使用键盘来控制Player的移动。双击打开Player脚本,输入以下内容:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public float speed = 10f; // 玩家的移动速度
public int maxHp = 10; // 玩家的最大血量
private int hp; // 玩家的当前血量
private bool idDead = false;
void Start()
{
hp = maxHp; // 初始血量为最大值
}
void Update()
{
if (!idDead)
{
Move();
}
}
void Move()
{
var input = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")); // 接受键盘输入
input = input.normalized;
Vector3 currentPos = transform.position;
currentPos += input * speed * Time.deltaTime;
if (input.magnitude > 0.1f)
{
transform.forward = input; // 使玩家面向移动方向
}
// 限制玩家的移动范围,范围为方圆20以内
const float distance = 20f;
if (currentPos.x > distance)
currentPos.x = distance;
if (currentPos.x < -distance)
currentPos.x = -distance;
if(currentPos.z > distance)
currentPos.z = distance;
if(currentPos.z < -distance)
currentPos.z = -distance;
transform.position = currentPos;
}
}
保存脚本,运行游戏,按键盘上的方向键或者A、D、S、W键,可以看到Player朝着指定的方向移动起来了。
注意:
以上移动方式没有考虑阻挡,为放置玩家或者敌人移动到地面的边缘掉下去,我们使用了简单的方式来限定他们的可移动范围。
运行游戏,发现Player在移动时,相机是固定的。下面我们把相机改为随着Player移动而移动。在《3D小球跑酷》我们采用的是把相机直接作为Player的子物体的方式,这里我们采用另一中方式(当然,你也可以使用作为子物体的方式),就是编写相机跟随脚本。
在Scripts文件夹下新建C#脚本,命名为FlowCamera。将脚本挂在到Camera上,双击打开脚本,输入以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlowCamera : MonoBehaviour
{
public Transform target;
Vector3 offset;
void Start()
{
offset = transform.position - target.position;
}
void Update()
{
transform.position = target.position + offset ;
}
}
将Player物体拖拽到脚本参数target字段。保存再次运行游戏,发现相机已经可以跟随Player移动了。
创建一个球体,大小缩放为0.2倍。命名为Bullet, 这将作为我们发射的子弹。新建脚本Bullet,将脚本挂在到Bullet物体上。双击打开脚本,输入以下内容:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float lifeTime = 2f; // 子弹的生命周期
public float speed = 10f; // 子弹的飞行速度
float startTime; // 子弹的生成时间
// Start is called before the first frame update
void Start()
{
startTime = Time.time;
}
// Update is called once per frame
void Update()
{
transform.position += speed * transform.forward * Time.deltaTime;
// 超过一定时间后
if (startTime + lifeTime < Time.time)
{
Destroy(gameObject); // 销毁子弹
}
}
}
将球体拖到Prefabs文件夹,做成预制体。
下面我们实现一下Player切换武器的功能,这是我们游戏的一个重要功能。我们按Q键可以切换武器。武器有三种,分别是手枪(Pistol),自动步枪(Rifle)和散弹枪(shotgun)。
新建脚本,命名为Weapon。双击打开脚本,输入以下内容:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public GameObject bulletPrefab; // 子弹预制体
public float pistolFireCD = 0.2f; // 手枪发射子弹的时间间隔
public float rifleFireCD = 0.1f; // 自动步枪发射子弹的时间间隔
public float shotgunFireCD = 0.5f; // 散弹枪发射子弹的时间间隔
float lastFireTime; // 上次开火时间
public int curGun; //当前使用的武器 0:手枪 1:自动步枪 2:散弹枪
// 开火方法,由角色脚本调用, keyDown 表示按一下就发射一颗子弹, keyPressed 表示长按
public void Fire(bool keyDown, bool keyPressed)
{
switch (curGun)
{
case 0:
if (keyDown)
{
PistolFire();
}
break;
case 1:
if (keyDown)
{
ShotgunFire();
}
break;
case 2:
if (keyPressed)
{
RifleFire();
}
break;
default:
break;
}
}
// 更换武器
public int ChangWeapon()
{
curGun += 1;
if (curGun >= 3)
curGun = 0;
return curGun;
}
// 手枪射击
private void PistolFire()
{
// 子弹发射时间间隔判定
if (lastFireTime + pistolFireCD > Time.time)
{
return;
}
lastFireTime = Time.time;
GameObject bullet = GameObject.Instantiate(bulletPrefab);
bullet.transform.position = transform.position + transform.forward * 1.0f; // 子弹的位置,位于角色前方1米
bullet.transform.forward = transform.forward; // 子弹的方向
}
// 自动步枪射击
private void RifleFire()
{
// 子弹发射时间间隔判定
if (lastFireTime + rifleFireCD > Time.time)
{
return;
}
lastFireTime = Time.time;
GameObject bullet = GameObject.Instantiate(bulletPrefab);
bullet.transform.position = transform.position + transform.forward * 1.0f; // 子弹的位置,位于角色前方1米
bullet.transform.forward = transform.forward; // 子弹的方向
}
// 散弹枪射击,一次发射5颗子弹,子弹间隔10
private void ShotgunFire()
{
// 子弹发射时间间隔判定
if (lastFireTime + shotgunFireCD > Time.time)
{
return;
}
lastFireTime = Time.time;
for (int i = -2; i <= 2; i++)
{
GameObject bullet = GameObject.Instantiate(bulletPrefab);
var dir = Quaternion.Euler(0, i * 10, 0) * transform.forward;
bullet.transform.position = transform.position + dir * 1.0f; // 子弹的位置,位于角色前方1米
bullet.transform.forward = dir; // 子弹的方向
}
}
}
修改Player脚本调用武器系统,使角色发射子弹。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
......
// 声明武器对象
private Weapon weapon;
void Start()
{
hp = maxHp; // 初始血量为最大值
weapon = GetComponent(); // 获取武器对象
}
void Update()
{
if (!idDead)
{
Move();
Fire(); // 开火,发射子弹
ChangeWeapon(); // 切换武器
}
}
void Move()
{
......
}
void Fire()
{
bool keyDown = Input.GetKeyDown(KeyCode.J);
bool keyPressed = Input.GetKey(KeyCode.J);
weapon.Fire(keyDown, keyPressed);
}
void ChangeWeapon()
{
bool changWeapon = Input.GetKeyDown(KeyCode.Q);
if (changWeapon)
weapon.ChangWeapon();
}
}
将武器脚本(Weapon)挂载到Player物体上,同时把子弹预制体拖到武器脚本的bulletPrefab字段上。保存场景,运行游戏,按Q键可以切换枪支,按J键可以发射子弹。读者可以自行调整玩家的移动速度和子弹的发射速度,以获得更好的游戏体验。
敌人脚本和Player脚本类似,单敌人会始终朝着玩家前进,并朝着玩家发射子弹。在Scripts文件夹下新建脚本文件,命名为Enermy,并将其挂载到Enermy预制体上。双击打开脚本,输入以下内容:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enermy : MonoBehaviour
{
private Transform playerTransform; // player 的 transform 组件
public float speed = 2f; // 敌人的移动速度
public int maxHp = 1; // 敌人的最大血量
private int hp; // 敌人的当前血量
private bool isDead = false;
private Weapon weapon;
void Start()
{
hp = maxHp; // 初始血量为最大值
weapon = GetComponent();
playerTransform = GameObject.Find("Player").transform;
}
void Update()
{
if (!isDead)
{
Move();
Fire();
}
}
void Move()
{
Vector3 dir = playerTransform.position - transform.position;
var currentPos = transform.position + dir.normalized * speed * Time.deltaTime;
// 限制敌人的移动范围,范围为方圆20以内
const float distance = 20f;
if (currentPos.x > distance)
currentPos.x = distance;
if (currentPos.x < -distance)
currentPos.x = -distance;
if (currentPos.z > distance)
currentPos.z = distance;
if (currentPos.z < -distance)
currentPos.z = -distance;
transform.position = currentPos;
transform.forward = dir.normalized;
}
void Fire()
{
weapon.Fire(true, true);
}
}
然后,把武器脚本挂载到Enermy预制体上,保存。再场景中放置几个敌人(直接把Enermy预制体拖到场景中即可),运行游戏,发现敌人开始朝着玩家移动,并不断发射子弹。
虽然现在我们可以控制Player移动,并且能发射子弹,敌人也能够发射子弹,但是子弹碰到敌人或者Player都没有反应,接下来我们就开始添加子弹碰撞的逻辑。
子弹和物体的碰撞分为以下几种情况:
1) Player的子弹击中敌人
2)敌人的子弹击中Player
3)玩家的子弹和Player的子弹碰撞
4)Player的子弹不会击中Player,敌人的子弹也不会击中敌人。
为了区分敌人子弹和Player的子弹,我们采用标签(Tag)来进行区分。我们新建两个标签,分别为PlayerBullet和EnermyBullet。选中之前的子弹预制体,重命名为PlayerBullet, 并将Tag设置为PlayerBullet。复制PlayerBullet,重命名为EnermyBullet,并将Tag设置为EnermyBullet。
双击Enermy预制体,将脚本Weapon上的BulletPrefab字段设置为EnermyBullet。保存退出。
双击Bullet脚本,添加碰撞逻辑。添加以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
.....
private void OnTriggerEnter(Collider other)
{
if (CompareTag(other.tag)) // 如果子弹的tag和碰撞到的物体的Tag相同
return;
Destroy(gameObject);
}
}
当敌人的子弹碰到玩家的时候,玩家血量减1,当血量为0时,玩家死亡,游戏结束。双击打来Player脚本,添加以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
......
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("EnermyBullet"))
{
if (hp <= 0) return;
hp--;
if (hp <= 0)
{
idDead = true;
Debug.Log("Game Over!");
}
}
}
}
当玩家的子弹碰到敌人的时候,敌人血量减1。当敌人的血量为0时,敌人死亡。双击打开Enermy脚本,添加以下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enermy : MonoBehaviour
{
......
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("PlayerBullet"))
{
if (hp <= 0) return;
hp--;
if (hp <= 0)
{
Destroy(gameObject);
}
}
}
}
保存场景,运行游戏,发现子弹碰撞后并没有什么效果,这是因为我们没有添加刚体,并设置触发器。
选中敌人的子弹和Player的子弹的预制体,添加刚体组件(RigidBody),并勾选 Collider 上的Is Trigger 复选框。选择Enermy预制体,并勾选 Collider 上的Is Trigger 复选框。选择Player,并勾选 Collider 上的Is Trigger 复选框。保存场景再次运行游戏,发现Player 被击中之后,血量减少,当血量减少到0是,控制台输出“Game Over”。
现在我们的游戏已经初具模型了,单要手动把敌人预制体拖到场景中,如果要有许多敌人就显得太麻烦了,而且拖到场景中的敌人有限,杀完了就没有了。 我们要有一个可以源源不断生成敌人的机制,每隔一段时间就生成一个敌人,这样就可以一直玩下去了。
在场景中新建一个空物体,命名为EnemySpawn,同时新建脚本文件,也命名为EnemySpawn,并挂载到EnemySpawn物体上。脚本内容如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnermySpawn : MonoBehaviour
{
public GameObject enermyPrefab;
public float spawTimer = 2.0f;
List enermies = new List ();
void Update()
{
if (enermies.Count >= 10) return; // 场景中最多10个敌人
spawTimer -= Time.deltaTime;
if (spawTimer <= 0)
{
spawTimer = 2.0f;
GameObject enermy = Instantiate(enermyPrefab);
enermy.transform.position = new Vector3(-20, 1, -20);
}
}
}
保存脚本和场景,运行游戏,发现没隔2秒就会有一个敌人生成,并且场景中最多会有10个敌人,当场景中的敌人数量少于10个时,又会有新的敌人生成出来,这样就可以一直玩下去了。
好了,射击游戏到这里就结束了,虽然还有很多不足,但作为初学者的练习案例,也用到了许多Unity的基础知识,希望您能够获得一些启发,对您的学习有所帮助。您也可以发挥自己的想想力,完善本案例,比如增加积分、音效等等,使游戏更加丰富。