目录
项目源码:xiaoma/ShootGame
一、简介
本次实验主要实现了第一人称视角下的射击游戏,玩家通过键盘控制角色的移动、通过鼠标控制视野的变化和射击效果实现对固定靶和移动靶的射击。
1、游戏规则
玩家可以在游戏中有两种武器可以选择分别是枪和弓弩。
当初始玩家选择武器为枪时,初始玩家弹匣中装满31颗子弹以及60颗备弹,弹匣的容量为31,当当前弹匣子弹数量为0时需要进行换弹,当子弹全部用尽时玩家无法继续射击需要重新开始游戏。在游戏中玩家可以使用子弹击中任意物体,但是只有击中Target(固定靶子)或者Sheep(移动靶)才可以获得相应的分数。对于target只要击中即可获得1分,target没有设置血量因此不会被销毁。每一个Sheep固定有3点血量,每击中一次可以减少其1点血量,当血量为0时该对象会被销毁玩家获得10点分数。
当玩家初始选择弓弩时需要先使用R按键装载弓箭,通过鼠标左键进行蓄力鼠标右键进行发射。由于弓弩击中Sheep对象的难度比较大因此不对Sheep采取血量而是直接击中即可得分。
2、界面UI
整个界面包括左上角的分数显示UI、左下角的子弹数量UI(弹匣中的子弹数量/备弹数量)、枪的准心以及集中靶子时的提示UI。右上角是角色移动的小地图,会根据角色的位置显示附近的物体(玩家以红色球体来显示)具体显示如下图:
图1、界面UI展示
图2、初始武器选择
3、按键设置
通用:
视角:通过鼠标的移动控制
行走:通过w、s、a、d控制角色的上下左右移动;
武器切换菜单: Q按键;
奔跑:通过左shift按键实现角色的奔跑;
跳跃:通过空格按键实现角色的跳跃;
换弹: 通过R按键实现换弹;
武器为枪时:
瞄准: 按住鼠标右键进入瞄准状态,松开鼠标右键退出瞄准状态;
射击:按下鼠标左键进行设计
查看武器: 通过F按键实现武器查看;
武器为弓弩时:
蓄力:长按鼠标左键完成蓄力;
发射:鼠标右键;
瞄准:键盘E;
二、具体实现
1、地图设计
使用unity中的资源商店下载Low-Poly Simple Nature Pack资源,并根据自己的喜好对场景中的树木岩石进行调整。同时将整个环境资源放在GroundLayer层(可以随机自己定义)方便后续的地面检测。
2、玩家角色设计
a、资源导入
在Unity资源商店中找到相应的枪械和动画资源。
b、玩家移动和视角设计
通过创建一个胶囊体(Player)作为玩家载体并将枪械预制体添加到该胶囊体上并调整好位置。为玩家添加一个摄像机(camare)命名为View控制玩家的视野变换,在玩家角色的底部放置一个空物体(groundCheck)用来作为玩家和地面的接触点检测。为玩家添加Character Controller(角色控制器)组件。
1)、视野控制
使用ViewController脚本控制玩家的视野:通过当前鼠标的水平和垂直方向的输入量来控制视野的左右上下移动,通过玩家本身的旋转变化来控制视野的旋转变化。我们需要玩家对象、鼠标的x轴方向的值、y轴方向的值,通过Clamp函数用来限制视野的水平方向上的最大最小移动角度。核心代码实现如下:
private void Update(){
//通过获取鼠标的坐标来实现视角的移动
mouseX = Input.GetAxis("Mouse X") * mouseSensitivity*Time.deltaTime;
mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity*Time.deltaTime;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation,-70f,70f);
//通过控制plyar的位置来实现视角的移动
player.Rotate(Vector3.up*mouseX);
//实现相机的旋转
transform.localRotation = Quaternion.Euler(xRotation, 0, 0);
}
2)、角色移动
通过获取键盘的水平和垂直方向的输入来控制角色的移动。在角色的移动过程中我们需要三个布尔变量来控制角色的移动速度来区分角色的行走、奔跑和跳跃 。在角色移动过程中我们还需要对角色进行地面检测,用来判断角色当前是否在地面上 。地面检测的原理就是利用物理射线,从当前的地面检查点的位置向下垂直发出一条物理射线,检测范围为0.1,检查的图层为groundLayer。同时为角色添加Audio Source组件通过获取Audio Clip资源来播放相应的移动音效。
public void CheckGround(){
isGrounded = Physics.CheckSphere(groundCheck.position, 0.1f, groundLayer);
if(isGrounded && velocity.y <= 0){
velocity.y = -2f;
}
}
角色跳跃通过判断键盘的空格是否有输入,并给玩家作用一个向上的力实现玩家的跳跃,在地面检测的函数中实现玩家的下落实现完整的跳越过程。
public void Jump(){
//获取按键的值
isjump = Input.GetButtonDown(jumpInputName);
if(isjump && isGrounded ){
velocity.y = Mathf.Sqrt(jumpForce* -2f * gravity);
}
}
具体的玩家其他相关设置可以通过查看项目的movecontroller脚本实现。
c、玩家射击实现
1)、射击的动画状态机设置
创建一个动画状态机根据不同的状态播放不同的动画,通过控制边的条件实现不同动画之间的切换。首先玩家进入游戏就会默认执行take_out_weapon动画,该动画是玩家拿出武器的动画,随后进入等待状态(Idel),通过inspect、Run、Walk、Aim四个布尔变量来控制武器检视、奔跑、行走、瞄准四个状态动画的播放与退出。
2)、射击脚本实现
射击的脚本为weaponController其中的核心方法是GunFire()用来实现射击。射击的本质也是从当前的准心的位置向前打出一条射线,在射线范围内如果击中物体则将集中的位置存储在hit中。在射击过程中会产生一些粒子效果比如:枪口的开火粒子特效、弹壳的抛出特效、子弹射击的火花特效、射中物体产生的弹孔粒子特效等。我们可以通过一下四个变量来获取和实现特效的播放与回收。
控制特效变量和射击核心代码:
public ParticleSystem muzzleFlash; //枪口特效
public Light MuzzleLight; //枪口光照
public GameObject hitParticle;//子弹射击的粒子效果
public GameObject bullectHole;
public void GunFire(){
//如果计时器的值小于射速
if(fireTimer < fireRate || currentBullets <= 0 || isReload || PM.isRunning){
return ;
}
//定义射击的方向
Vector3 shootDirection = shooterPoint.forward;
RaycastHit hit;
//使用射线检测 从shooterPoint射向范围内的物体如果击中了一些物体 则将hit.point赋值给hit.point
if(Physics.Raycast(shooterPoint.position,shootDirection,out hit,range)){
Debug.Log(hit.collider.transform.name+"被击中了");
//创建子弹击中物体的火花特效
GameObject hitParticleEffect = Instantiate(hitParticle,hit.point,Quaternion.FromToRotation(Vector3.up,hit.normal));
//实例化弹孔特效
GameObject bullectHoleEffect = Instantiate(bullectHole,hit.point,Quaternion.FromToRotation(Vector3.up,hit.normal));
GameObject hittedObject = hit.collider.gameObject;
bullectHoleEffect.transform.SetParent(hittedObject.transform);
if(hittedObject.CompareTag("Animal")){
hittedObject.GetComponents<IDamageable>()[0].TakeDamage(1); //调用被击中物体的TakeDamage方法
Debug.Log(hittedObject.name+"Animal被击中了");
}
if(hittedObject.CompareTag("Target")){
string message = "target is hitted!score+1";
hitUI.ShowHitMessage(message);
scoreController.AddScore(1); //调用被击中物体的TakeDamage方法
}
//特效的回收
Destroy(hitParticleEffect,1f);
Destroy(bullectHoleEffect,3f);
}
//是否瞄准状态
if(!isAiming){
//对动画状态进行淡入淡出的动画
animator.CrossFadeInFixedTime("fire",0.1f);
}else{
//瞄准状态下的开火动画
animator.CrossFadeInFixedTime("aim_fire",0.1f);
}
PlayerShootSound(); //播放射击音效
MuzzleLight.enabled = true; //播放枪口光照
muzzleFlash.Play(); //播放枪口特效
//抛出弹壳动画
Instantiate(casingPrefab,casingSpawnPoint.transform.position,casingSpawnPoint.transform.rotation);
//扣除一个弹药
currentBullets --; //扣除一个弹药
UpdateAmmoUI();
//射击完成的时候重置计时器
fireTimer = 0f;
}
射击效果展示
瞄准本质上就是摄像机的位置的变换,将摄像机的位置相对于角色位置往前移动即可将视野拉近实现瞄准,在进入瞄准状态时我们需要隐藏准心。具体代码实现如下:
public void DoingAim(){
//瞄准判断
if(Input.GetMouseButton(1) && !isReload && !PM.isRunning){
isAiming = true;
animator.SetBool("Aim",true);
//准心消失
CrossHairUI.gameObject.SetActive(false);
//视野前移
mainCamera.fieldOfView = 25;//瞄准的视野
}else{
isAiming = false;
animator.SetBool("Aim",false);
//准心出现
CrossHairUI.gameObject.SetActive(true);
mainCamera.fieldOfView = 60;//瞄准的视野
}
}
非瞄准状态效果展示
瞄准状态效果展示
5)、弓弩射击实现
a、动画状态机——采用混合树实现半蓄力和蓄力
动画状态机
b、脚本控制核心代码
采用基于动作分离版的实现,通过箭矢的飞行动作管理基类实现发射的箭矢的飞行控制,在ArrowController脚本中控制弓弩发射弓箭。箭矢采用对象池技术进行管理和回收。
public class ArrowFactory : MonoBehaviour
{
public ArrowController arrowCtrl;
public GameObject arrow;
void Start()
{
arrowCtrl = FindObjectOfType<ArrowController>();
}
void Update(){
}
public GameObject GetArrow(){ // 获取空闲的箭
arrow = GameObject.Instantiate(Resources.Load("Prefab/Arrow", typeof(GameObject))) as GameObject;
if(arrow == null){
Debug.Log("箭预制体为空");
}
//得到弓箭上搭箭的位置
Transform bow_mid = arrowCtrl.bow.transform.GetChild(4); // 获得箭应该放置的位置
arrow.transform.position = bow_mid.transform.position; //将箭的位置设置为弓中间的位置
arrow.transform.rotation = arrowCtrl.bow.transform.rotation; // 将箭的旋转角度设置为弓的旋转角度
arrow.transform.parent = arrowCtrl.bow.transform; //箭随弓的位置变化
arrow.gameObject.SetActive(false);
return arrow;
}
public void RecycleArrow(GameObject arrow) // 回收箭
{
arrow.SetActive(false);
DestroyImmediate(arrow);
}
}
ArrowController脚本中实现射击的核心代码:
//ArrowCOntroller核心代码实现
void aniShoot(){
if(Input.GetMouseButtonDown(0) && state == 0){
transform.GetChild(4).gameObject.SetActive(true);//显示拉弓动画
isLongPressing = true;
animator.SetTrigger("pull");
pressTime = Time.time;
state = 1;
PlayeSound(1);
}
//鼠标左键抬起
if(Input.GetMouseButtonUp(0) && state == 1){
isLongPressing = false;
float duration = Time.time - pressTime;
//未蓄满力度
if(duration < longPressDuration){
power = duration/2;
}else{
power = 1.0f;
}
animator.SetFloat("power", power);
animator.SetTrigger("hold");
state = 2;
}
if(isLongPressing && Time.time - pressTime > longPressDuration)
{
isLongPressing = false;
power = 1.0f;
animator.SetFloat("power", power);
animator.SetTrigger("hold");
}
if(Input.GetMouseButtonDown(1) && state == 2)
{
transform.GetChild(4).gameObject.SetActive(false);//隐藏拉弓动画
animator.SetTrigger("shoot");
arrow = arrows[0];
arrow.SetActive(true);
flyed.Add(arrow);
arrows.RemoveAt(0);
if(arrow==null)
{
message = "no arrow!";
// hitUI.ShowHitMessage(message);
Debug.Log(message);
}
shootManager.ArrowShoot(arrow,mainCamera.transform.forward,power);
Debug.Log(mainCamera.transform.forward);
animator.SetFloat("power", 1.0f);//恢复力度
arrowNum-=1;
state = 0;
isAming = false;
PlayeSound(2);
}
}
3、移动靶和固定靶设计
1、固定靶
固定靶采用一个圆柱体实现,通过控制圆柱体的高和旋转将其制作成靶子,并使用贴图为其添加外观。为固定靶添加一个碰撞器,方便后续的射线检测。
2、移动靶(Sheep)
a、资源导入
通过unity资源商店导入Sheep的预制资源(可以自行下载选择其他资源)将预制体放入到场景中并控制好位置。为每一个预制体添加一个碰撞器并调整碰撞器的大小保证能够覆盖整个预制体。
b、移动靶的自动寻路设置
对于Sheep我们采用Unity中的AI中的NavMesh Agent智能体实现每一个Sheep对象的自动寻路实现Sheep的随机移动 。首先我们需要为我们的环境资源添加一个NavMeshSurface组件用来计算地图中可以移动的网格,通过为环境中的其他资源比如:树、岩石等添加NavMeshObstacle组件来阻止Sheep对象将他们的位置作为移动的目标。同时我们还必须为我们的移动目标Sheep添加一个NavMeshAgent组件实现自动寻路功能。
我们通过随机的为每一个Sheep对象随机的生成一个目标地点,并让Sheep对象移动到该地点,并随机设置一定的空闲时间。对于空闲等待和移动到 目标地点我采取协程处理的方式,具体来说,这段代码通过 StartCoroutine
启动协程,并在 moveToDestination
方法中通过 yield return null
每帧暂停,直至达到目的地或超时。这样可以避免在主线程中进行耗时的计算,从而保持游戏的流畅性 。对于空闲等待状态也是采用相同的技术随机生成一定的空闲等待时间。
private void HandleIdle(){
StartCoroutine(waitToMove());
}
private IEnumerator waitToMove(){
float waitTime = UnityEngine.Random.Range(IdleTime/2,IdleTime*2);
yield return new WaitForSeconds(waitTime);
Vector3 randomDestination = GetRandomDestination(transform.position, HanderDistance);
agent.SetDestination(randomDestination);
SetState(AnimalState.Move);
}
private Vector3 GetRandomDestination(Vector3 origin,float distance){
for(int i=0;i<5;i++){
Vector3 RandomDirection =UnityEngine.Random.insideUnitSphere * distance;
RandomDirection += origin;
UnityEngine.AI.NavMeshHit hit;
if(UnityEngine.AI.NavMesh.SamplePosition(RandomDirection,out hit,distance,UnityEngine.AI.NavMesh.AllAreas)){
return hit.position;
}
}
return origin;
}
private void HandleMove(){
StartCoroutine(moveToDestination());
}
private IEnumerator moveToDestination(){
float startTime = Time.time;
//如果在步行最大时间内没有到达目的地就会重新规划路线
while(agent.remainingDistance > agent.stoppingDistance ){
if(Time.time - startTime > maxMoveTime){
agent.ResetPath();
SetState(AnimalState.Idle);
yield break;
}
yield return null;
}
//到达目的地后停止移动
SetState(AnimalState.Idle);
}
c、对象的销毁
对于血量低于0的Sheep对象采用事件监听机制,注册销毁行为的触发器。其中OnDestroy
在对象被销毁时自动调用。在此方法中,过 OnSheepDestroy?.Invoke(gameObject);
触发 OnSheepDestroy
事件,并将当前的 gameObject
(即被销毁的羊对象)作为参数传递给所有订阅了这个事件的处理程序。?.
是 null 条件运算符,确保只有在有订阅者的情况下才调用该事件。同时实现该事件的监听器对Sheep对象被销毁时的事件进行监听,并执行对应的销毁操作和分数更i性能操作。
public static event Action<GameObject> OnSheepDestroy;
private void OnDestroy(){
OnSheepDestroy?.Invoke(gameObject);
}
public class DistoryListener : MonoBehaviour
{
public ScoreController scoreController;
private void OnEnable(){
SheepController.OnSheepDestroy += HandleSheepDestroy;
scoreController = FindObjectOfType<ScoreController>();
}
private void OnDisable(){
SheepController.OnSheepDestroy -= HandleSheepDestroy;
}
private void HandleSheepDestroy(GameObject sheep){
Destroy(sheep);
scoreController.AddScore(5);
}
}
d、动画状态机设置
利用资源里的动画设置相应的动画状态机控制Sheep对象的移动和等待、死亡等状态。
4、小地图设计
采用掩膜和多摄像机结合的方式在UI界面设计RawImag来实现。
5、分数计算
设计ScoreController类来实现全局的分数控制。在使用弓弩进行设计时的计分由于弓箭采用了刚体组件因此我们采用碰撞检测机制来实现计分。
void OnTriggerEnter(Collider collision){
Transform arrow = collision.gameObject.transform;
if(arrow == null) return;
if(arrow.tag == "Arrow"){
arrow.GetComponent<Rigidbody>().isKinematic = true;
arrow.transform.rotation = Quaternion.Euler(0,0,0);
arrow.transform.parent = this.transform;
arrow.tag = "onTarget";
// currentState = AnimalState.Died;
animator.SetTrigger("Die");
OnDestroy();
}else{
return ;
}
}