- 博客主页:https://blog.csdn.net/zhangay1998
- 欢迎点赞 收藏 ⭐留言 如有错误敬请指正!
- 本文由 God Y.原创,首发于 CSDN
- 未来很长,值得我们全力奔赴更美好美好的生活✨
既然想要做一个CS那样的射击游戏,场景跟模型肯定得有啊
我们这就来导入一个简单的资源包,里面有简单的一个场景和几把枪械
首先,我们在场景中新建一个游戏对象GameObject,将其改名为FPSController
然后给他添加上角色控制器Character Controller和Audio Source
角色控制器Character Controller可以更容易的处理有碰撞的运动
Audio Source 是 Unity 中的 Audio 组件,其主要用来播放游戏场景中的 - AudioClip,AudioClip 就是导入到 Unity 中的声音文件。
其次,将场景中默认创建的Camera删掉或者取消激活,我们这里会自己新建一个Camera使用,所以暂时用不到这个默认的相机
在这个游戏对象下再新建一个GameObject,改名为FirstPersonCharacter,给他添加上Camera和AudioListener组件
这个Camera就是我们能看到的第一视角,AudioListener是游戏中的声音接收器,用来接收场景中的声音,一般都挂在相机上
主要是控制人物的行走、移动、跳跃
还有人物身上的一些声音的控制
定义一个值用来声明人物是否是行走状态,再定义两个参数分别用来用来处理移动速度和奔跑速度
然后还有对第一人称视角的控制
以及几个声音数组,用来控制人物行走,奔跑和跳跃的不同声音
这块地方实现的方法有好多种,所以这里只是提供一种方法作为参考
这里写的有点多,嫌麻烦的可直接复制这个脚本的代码挂在我们的FPSController身上即可
直接上代码,挂载到我们的角色控制器FPSController上就行
using System;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
using UnityStandardAssets.Utility;
using Random = UnityEngine.Random;
namespace UnityStandardAssets.Characters.FirstPerson
{
[RequireComponent(typeof (CharacterController))]
[RequireComponent(typeof (AudioSource))]
public class FirstPersonController : MonoBehaviour
{
[SerializeField] private bool m_IsWalking;
[SerializeField] private float m_WalkSpeed;
[SerializeField] private float m_RunSpeed;
[SerializeField] [Range(0f, 1f)] private float m_RunstepLenghten;
[SerializeField] private float m_JumpSpeed;
[SerializeField] private float m_StickToGroundForce;
[SerializeField] private float m_GravityMultiplier;
[SerializeField] private MouseLook m_MouseLook;
[SerializeField] private bool m_UseFovKick;
[SerializeField] private FOVKick m_FovKick = new FOVKick();
[SerializeField] private bool m_UseHeadBob;
[SerializeField] private CurveControlledBob m_HeadBob = new CurveControlledBob();
[SerializeField] private LerpControlledBob m_JumpBob = new LerpControlledBob();
[SerializeField] private float m_StepInterval;
[SerializeField] private AudioClip[] m_FootstepSounds; //从中随机选择的一组足迹声音。
[SerializeField] private AudioClip m_JumpSound; //角色离开地面时发出的声音。
[SerializeField] private AudioClip m_LandSound; //角色触地时播放的声音。
private Camera m_Camera;
private bool m_Jump;
private float m_YRotation;
private Vector2 m_Input;
private Vector3 m_MoveDir = Vector3.zero;
private CharacterController m_CharacterController;
private CollisionFlags m_CollisionFlags;
private bool m_PreviouslyGrounded;
private Vector3 m_OriginalCameraPosition;
private float m_StepCycle;
private float m_NextStep;
private bool m_Jumping;
private AudioSource m_AudioSource;
private void Start()
{
m_CharacterController = GetComponent<CharacterController>();
m_Camera = Camera.main;
m_OriginalCameraPosition = m_Camera.transform.localPosition;
m_FovKick.Setup(m_Camera);
m_HeadBob.Setup(m_Camera, m_StepInterval);
m_StepCycle = 0f;
m_NextStep = m_StepCycle/2f;
m_Jumping = false;
m_AudioSource = GetComponent<AudioSource>();
m_MouseLook.Init(transform , m_Camera.transform);
}
private void Update()
{
RotateView();
// 跳转状态需要在这里读取,以确保它不会丢失
if (!m_Jump)
{
m_Jump = CrossPlatformInputManager.GetButtonDown("Jump");
}
if (!m_PreviouslyGrounded && m_CharacterController.isGrounded)
{
StartCoroutine(m_JumpBob.DoBobCycle());
PlayLandingSound();
m_MoveDir.y = 0f;
m_Jumping = false;
}
if (!m_CharacterController.isGrounded && !m_Jumping && m_PreviouslyGrounded)
{
m_MoveDir.y = 0f;
}
m_PreviouslyGrounded = m_CharacterController.isGrounded;
}
private void PlayLandingSound()
{
m_AudioSource.clip = m_LandSound;
m_AudioSource.Play();
m_NextStep = m_StepCycle + .5f;
}
private void FixedUpdate()
{
float speed;
GetInput(out speed);
//始终沿着相机向前移动,因为这是它瞄准的方向
Vector3 desiredMove = transform.forward*m_Input.y + transform.right*m_Input.x;
//获取被接触曲面的法线以沿其移动
RaycastHit hitInfo;
Physics.SphereCast(transform.position, m_CharacterController.radius, Vector3.down, out hitInfo,
m_CharacterController.height/2f, Physics.AllLayers, QueryTriggerInteraction.Ignore);
desiredMove = Vector3.ProjectOnPlane(desiredMove, hitInfo.normal).normalized;
m_MoveDir.x = desiredMove.x*speed;
m_MoveDir.z = desiredMove.z*speed;
if (m_CharacterController.isGrounded)
{
m_MoveDir.y = -m_StickToGroundForce;
if (m_Jump)
{
m_MoveDir.y = m_JumpSpeed;
PlayJumpSound();
m_Jump = false;
m_Jumping = true;
}
}
else
{
m_MoveDir += Physics.gravity*m_GravityMultiplier*Time.fixedDeltaTime;
}
m_CollisionFlags = m_CharacterController.Move(m_MoveDir*Time.fixedDeltaTime);
ProgressStepCycle(speed);
UpdateCameraPosition(speed);
m_MouseLook.UpdateCursorLock();
}
private void PlayJumpSound()
{
m_AudioSource.clip = m_JumpSound;
m_AudioSource.Play();
}
private void ProgressStepCycle(float speed)
{
if (m_CharacterController.velocity.sqrMagnitude > 0 && (m_Input.x != 0 || m_Input.y != 0))
{
m_StepCycle += (m_CharacterController.velocity.magnitude + (speed*(m_IsWalking ? 1f : m_RunstepLenghten)))*
Time.fixedDeltaTime;
}
if (!(m_StepCycle > m_NextStep))
{
return;
}
m_NextStep = m_StepCycle + m_StepInterval;
PlayFootStepAudio();
}
private void PlayFootStepAudio()
{
if (!m_CharacterController.isGrounded)
{
return;
}
// 从数组中拾取并播放随机的足迹声音,不包括索引0处的声音
int n = Random.Range(1, m_FootstepSounds.Length);
m_AudioSource.clip = m_FootstepSounds[n];
m_AudioSource.PlayOneShot(m_AudioSource.clip);
// 将拾取的声音移动到索引0,以便下次不再拾取
m_FootstepSounds[n] = m_FootstepSounds[0];
m_FootstepSounds[0] = m_AudioSource.clip;
}
private void UpdateCameraPosition(float speed)
{
Vector3 newCameraPosition;
if (!m_UseHeadBob)
{
return;
}
if (m_CharacterController.velocity.magnitude > 0 && m_CharacterController.isGrounded)
{
m_Camera.transform.localPosition =
m_HeadBob.DoHeadBob(m_CharacterController.velocity.magnitude +
(speed*(m_IsWalking ? 1f : m_RunstepLenghten)));
newCameraPosition = m_Camera.transform.localPosition;
newCameraPosition.y = m_Camera.transform.localPosition.y - m_JumpBob.Offset();
}
else
{
newCameraPosition = m_Camera.transform.localPosition;
newCameraPosition.y = m_OriginalCameraPosition.y - m_JumpBob.Offset();
}
m_Camera.transform.localPosition = newCameraPosition;
}
private void GetInput(out float speed)
{
// Read input
float horizontal = CrossPlatformInputManager.GetAxis("Horizontal");
float vertical = CrossPlatformInputManager.GetAxis("Vertical");
bool waswalking = m_IsWalking;
#if !MOBILE_INPUT
// 在独立构建中,步行/跑步速度通过按键进行修改。
//跟踪角色是在行走还是在跑步
m_IsWalking = !Input.GetKey(KeyCode.LeftShift);
#endif
// set the desired speed to be walking or running
speed = m_IsWalking ? m_WalkSpeed : m_RunSpeed;
m_Input = new Vector2(horizontal, vertical);
// 如果组合长度超过1,则规范化输入:
if (m_Input.sqrMagnitude > 1)
{
m_Input.Normalize();
}
// 只有当运动员准备跑步、正在跑步并且要使用fovkick时,处理速度变化以提供fovkick
if (m_IsWalking != waswalking && m_UseFovKick && m_CharacterController.velocity.sqrMagnitude > 0)
{
StopAllCoroutines();
StartCoroutine(!m_IsWalking ? m_FovKick.FOVKickUp() : m_FovKick.FOVKickDown());
}
}
private void RotateView()
{
m_MouseLook.LookRotation (transform, m_Camera.transform);
}
private void OnControllerColliderHit(ControllerColliderHit hit)
{
Rigidbody body = hit.collider.attachedRigidbody;
//如果角色在刚体上,不要移动刚体
if (m_CollisionFlags == CollisionFlags.Below)
{
return;
}
if (body == null || body.isKinematic)
{
return;
}
body.AddForceAtPosition(m_CharacterController.velocity*0.1f, hit.point, ForceMode.Impulse);
}
}
}
在这个资源包中已经有基本的动画片段了,如下图,我们要做的就是使用Animation Controller将动画片段控制起来
拿MP5举例,看一下动画片段
动画控制值Animator配置
这里我们先来给神器AK47(阿卡47)来进行动画配置
点开这个Animation Controller就是这个默认界面
这里一同样的方式来给MP5和喷子设置上动画控制器,方法都是一样的
小云儿:小Y哥哥,这样动画控制器就配置好啦,接下来就是使用代码和实现动画的调用了对叭!
小Y:没错,因为动画片段都是现成的,我们只需要把这个动画控制器 Animation Controller做好就可以了,是不是很简单呢~
小云儿:动画很流畅,就是这个开火换弹太慢了,我想枪射的更快一点,有什么办法解决吗?
小Y:Emma…有办法的!简单给你说一下,但是射太快也有缺点哦,就是子弹浪费的太快了哈哈~
我们在这个动画窗口中选择对应的动画状态,然后右面的属性面板中有一个Speed属性
从名字就可以知道他是控制动画的播放速度的,所以我们在这里将速度的值改的越大,它播放的速度就越快,那我们的换弹速度就会更快啦!
其实这一块也很简单
我们首先在FPSController下新建一个GameObject,改名为:GunMansger
然后将AK47、MP5和喷子这三把枪的预制体拖到这个GunMansger下,并根据摄像机视角调整好视角。
我们这里只演示这三把枪,其实再多也一样,就是体力活啦~
如下图所示:
将三把枪的位置和视角都调整好之后,就开始上代码了
首先 ,写一个挂在每个枪械上的脚本FPSGun,用于控制开火和换枪,包括动画和声音的播放
代码很简单,先在Awake中找到挂载该脚本的游戏对象身上的Animator组件
在OnEnable中播放readyClip音频,readyClip是切换到当前枪械时的声音
然后定义了三个方法,分别是Fire、Reload和Switch,自然是开火、换弹和换枪的方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FPSGun : MonoBehaviour
{
private Animator animator;
public AudioClip fireClip;
public AudioClip reloadClip;
public AudioClip readyClip;
private void Awake()
{
animator = GetComponent<Animator>();
}
private void OnEnable()
{
//播放准备声音
AudioSource.PlayClipAtPoint(readyClip,transform.position);
}
public void Fire()
{
//将开火名称转换为哈希
int fire = Animator.StringToHash("Fire");
//播放开火动画
animator.SetTrigger(fire);
//播放声音
AudioSource.PlayClipAtPoint(fireClip ,transform.position);
}
public void Reload()
{
int reload = Animator.StringToHash("Reload");
animator.SetTrigger(reload);
//播放声音
AudioSource.PlayClipAtPoint(reloadClip, transform.position);
}
public void Switch()
{
//播放声音
AudioSource.PlayClipAtPoint(readyClip, transform.position);
}
}
将对应的枪械声音添加到各自枪械身上:
然后是管理枪支的脚本GunManger,这个脚本挂在我们刚创建的游戏对象GunMansger身上
在这个脚本中,我们要做的事情也很简单
就是管理枪械的切换和控制开火、换弹
在FPSGun 脚本中我们之定义了方法,并没与调用,所以正是这个脚本中调用了
这里有个小细节就是一定要在Awake中创建列表,然后再Start中将枪械添加上去
因为Awake和Start都是在程序运行的第一帧运行,但是Awake比Start执行的早!
上代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GunManger : MonoBehaviour
{
//管理枪支
private List<FPSGun> managerdGuns;
//当前的枪支
private FPSGun currentGun;
//枪支编号
private int index=0;
private void Awake()
{
managerdGuns = new List<FPSGun>();
}
private void Start()
{
for (int i = 0; i < transform.childCount; i++)
{
//添加每一把枪
managerdGuns.Add(transform.GetChild(i).GetComponent<FPSGun>());
}
if (managerdGuns.Count>0)
{
//当前先使用的第一把枪
currentGun = managerdGuns[index];
}
}
private void Update()
{
//当前是否有一把枪
if (!currentGun)
{
return;
}
if (Input.GetButtonDown("Fire"))
{
currentGun.Fire();//开火
}
if (Input.GetButtonDown("Reload"))
{
currentGun.Reload();//换单
}
if (Input.GetButtonDown("Switch"))
{
currentGun.Switch();//换枪
currentGun.gameObject.SetActive(false);
//方法二
//计算新枪的编号
index = ++index % managerdGuns.Count;
currentGun = managerdGuns[index];//更新当前枪支
currentGun.gameObject.SetActive(true);//新枪
}
}
}
让我们再来添加一个简单的开火特效吧!
在资源包中有一个叫MuzzleFlash的预设体,他是一个简单的材质搭配了Shader做的,我们这里直接拿来用就好了
添加一个特效也有很多种方法,我们这里采用一个超级简单的办法
就是直接将这个预制体拖到枪械上,并摆放好合适的位置
如下图所示,直接放枪口处即可:
或者更夸张一点就下图这样,随便怎么高兴怎么设计,这就是自己开发游戏的快乐吧!
///
/// 播放开火特效
///
public void PlayerFireEffect()
{
//启动特效
muzzleFlash.SetActive(true);
//0.2秒关闭
Invoke("UnEffect", 0.1f);
}
///
/// 取消特效
///
private void UnEffect()
{
muzzleFlash.SetActive(false);
}
然后新建一个脚本PlayerAudioAndEffect,这个脚本有点特殊,因为他不是挂在游戏对象身上的,而是挂在动画片段身上的,创建完脚本后会直接继承StateMachineBehaviour,然后会有几个override重写方法,我们只需要在OnStateEnter方法中使用即可
代码也很简单,获取到当前的动画,然后在播放动画的时候展示特效即可,声音也可以在这里设置:
using UnityEngine;
public class PlayerAudioAndEffect : StateMachineBehaviour
{
public bool fire = true;
public bool reload = true;
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateinfo, int layerindex)
{
FPSGun currentGun = animator.GetComponent<FPSGun>();
if (fire)
{
//获取开火声音
AudioClip fireClip = currentGun.fireClip;
//播放
AudioSource.PlayClipAtPoint(fireClip, animator.transform.position);
//播放特效
currentGun.PlayerFireEffect();
}
if (reload)
{
//获取换单声音
AudioClip reloadClip = currentGun.reloadClip;
//播放
AudioSource.PlayClipAtPoint(reloadClip, animator.transform.position);
}
}
}
小Y:每当进行到这一步,就是我们最激动人心的时刻啦!
小云儿:是的呢 小Y哥哥,快来给我演示一下成果吧~
小Y:好咧!
动图演示
自制第一人称射击游戏简单游戏视频
文中没有做的地方还有挺多,比如一把枪的子弹限制,还有近战武器,投掷物和地图等等。
这些设置起来也都是比较麻烦的,但是作为一个第一人称射击游戏来说,本文中已经把最基础重要的功能给实现啦。
如果你想开发一款属于自己的射击游戏,那本文章可以作为一个启蒙Demo哦,可以让你知道原来网上一些很火的游戏我们也可以自己动手做!
该Demo的工程资源包在这里可以下载,感兴趣的小伙伴赶紧去试一下吧!