1、导入素材
1.1 素材
1.2 创建游戏对象
新建3d游戏对象Plane,作为地面
挂载贴图,调整地面尺寸
2、实现人物移动
2.1 创建胶囊体游戏对象
自带胶囊碰撞体
为此对象创建脚本,添加角色控制器组件并去掉自带胶囊碰撞体组件
2.2 写移动脚本
移动物体的方法有两种1、CharacterController组件实现2、RigidBody刚体实现
public float moveSpeed = 10f;//移动速度
public Vector3 moveDirection;//三维向量,表示移动方向
public void PlayerMove()
{
float h_Value= Input.GetAxis("Horizontal");
float v_Value = Input.GetAxis("Vertical");
moveDirection = (transform.right * h_Value + transform.forward * v_Value).normalized;//红色方向轴和蓝色方向轴,形成三维方向向量,并标准化
characterController.Move(moveDirection*moveSpeed*Time.deltaTime);
}
3、实现镜头跟随
3.1 使主摄像机成为Player的子物体
调整坐标位置使摄像机刚好在Player头上,控制Player就可以控制相机
3.2 创建脚本控制相机旋转
将脚本挂载到相机上,使用InputManager的鼠标控制X轴和Y轴
public float mouseSensitivity = 100f;//视角灵敏度
public Transform playerPos;//玩家位置
public float xRotation = 0f;//存放累加值
// Start is called before the first frame update
void Start()
{
//隐藏光标,避免画面中出现鼠标
Cursor.lockState = CursorLockMode.Locked;
}
// Update is called once per frame
void Update()
{
//将轴映射到鼠标,值为当前鼠标增量乘以轴灵敏度
float mouseX= Input.GetAxis("Mouse X")*mouseSensitivity*Time.deltaTime;
float mouseY= Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;//坐标跟随鼠标移动,上下是Y,左右是X
print("Mouse X:" + mouseX);
print("Mouse Y:" + mouseY);
//玩家左右旋转视角
playerPos.Rotate(Vector3.up* mouseX);//(0,1,0),物体绕Y轴横向旋转
//相机旋转
xRotation -= mouseY;//累加绕X轴上下旋转的轴值
xRotation = Mathf.Clamp(xRotation, -80f, 80f);//限制旋转角度(轴值累积)
transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);//相机绕X轴上下旋转改变角度
}
4、实现人物奔跑
可以动态改变速度,分别声明行走速度和奔跑速度,获取键盘(KeyCode)输入
ps:声明变量前加上[Tooltip("xxx")],即可在面板对应变量悬停显示提示信息;[SerializeField]强制序列化,使私有变量显示在面板;[Header("xxx")]给变量加上标题,方便后续区分变量用途
public float moveSpeed = 10f;//移动速度
public float walkSpeed = 20f;//按键区分速度
public float runSpeed = 15f;
public bool isRun;//奔跑判断
[Header("按键设置")]
public KeyCode runKeyInput;//奔跑键,左shift
public void PlayerMove()
{
isRun = Input.GetKey(runKeyInput);
if(isRun)
{
moveSpeed = runSpeed;
}
else
{
moveSpeed = walkSpeed;
}
}
5、实现人物跳跃
Y轴坐标矢量变换,需要向上的力和向下的重力
5.1 向上
public bool isJump;
public float jumpForce = 3f;//跳跃力度
public Vector3 velocity;//力,Y轴的一个冲量变化
public KeyCode jumpKeyInput;//跳跃键,空格
public void PlayerMove()
{
PlayerJump();
characterController.Move(velocity * Time.deltaTime);
}
public void PlayerJump()
{
isJump = Input.GetKey(jumpKeyInput);
if(isJump)
{
velocity.y = jumpForce * 2f;
}
}
5.2 增加地面检查点
- 当物体与地面碰撞时,说明物体已经落地,向下的重力消失
- 新建Player的子物体checkGround
- 新建地面检测方法在移动前调用并每帧执行
void Update()
{
GroundCheck();
PlayerMove();
}
- 为地面设置Layer层级
public bool isGround;//地面碰撞判断
private Transform groundCheck;//地面检测点,初始化CheckGround
private float groundDistance = 0.1f;//与地面的距离,私有变量
public LayerMask groundMesh;//地面层级,使小球与地面一个层,所以物理小球只与地面碰撞
public void GroundCheck()
{
//生成物理球体与地面碰撞,球体位置,半径,层级,层级与地面相同
//碰撞点是CheckGround坐标,调整其位置到接近地面处
isGround= Physics.CheckSphere(groundCheck.position,groundDistance,groundMesh);
//Debug.Log(isGround);
//在地面时给一个向下的力
if(isGround &&velocity.y<=0)
{
velocity.y = -2f;
}
}
5.3 起跳后向下
public float jumpForce = 3f;//跳跃力度
public float gravity = -20f;//向下的重力
public Vector3 velocity;//力,Y轴的一个冲量变化
/// <summary>
/// 移动
/// </summary>
public void PlayerMove()
{
if(isGround==false)//不接地,在空中累加向下的力
{
velocity.y += gravity * Time.deltaTime;
}
characterController.Move(velocity * Time.deltaTime);
PlayerJump();
}
/// <summary>
/// 跳跃
/// </summary>
public void PlayerJump()
{
isJump = Input.GetKey(jumpKeyInput);
if(isJump&&isGround)//按下空格并且在地上
{
velocity.y =Mathf.Sqrt ( jumpForce * -2f*gravity);
}
}
6、人物上坡
cube搭建一个斜坡
运行发现人物无法在斜坡上跳跃:修改groundCheck坐标位置,或坐标到地面的半径大小
Player在斜坡上行走不平稳,解决:检测Player在斜坡上时,额外施加一个向下的力
public bool OnSlpe()
{
if(isJump)
return false;
RaycastHit hit;//存放反射出的射线
//向下打出射线
//(Player)向场景中所有碰撞体投射一条射线,反射射线不垂直向上(0,1,0),则Player在斜边
if(Physics.Raycast(transform.position,Vector3.down,out hit,characterController.height/2*slopForcrRayLength))
{
if(hit.normal!=Vector3.up)//normal是法线,方向与(0,1,0)不一致
{
return true;
}
}
return false;
}
if(OnSlpe())//如果在斜坡
{
characterController.Move(Vector3.down * characterController.height / 2 * slopForce * Time.deltaTime);
}
7、实现射击
7.1 找到素材中枪械预制体
预制体挂载到主相机下,采用射线检测实现射击的发生
枪械位置归零
修改摄像机渲染距离(Clipping Planes),解决枪视角穿模
添加UI,image作为准心
public Transform shootPoint;//武器射击位置(子弹出膛位置
public int range = 100;//子弹射程
//右键瞄准,左键射击
private bool gunShootInput;
void Update()
{
gunShootInput = Input.GetMouseButton(0);//左键按下
if(gunShootInput)
{
GunFire();
}
}
/// <summary>
/// 射击
/// </summary>
public void GunFire()
{
//向前射击
Vector3 shootDirection = shootPoint.forward;
RaycastHit hit;
//发出射线
if(Physics.Raycast(shootPoint.position,shootDirection,out hit,range))//击中目标信息存入hit
{
Debug.Log(hit.transform.name+"打到惹");
}
}
发出的射线击中目标物体(但是可以对Player对象进行射击)
在Update函数一秒60帧,需要自己手动控制射速
7.2 控制射速
//射速控制,计时器
public float fireRate = 0.1f;
public float fireTimer;
void Update()
{
if(fireTimer<fireRate)//射速越小,打出的射线越少
{
fireTimer += Time.deltaTime;
}
}
public void GunFire()
{
//计时器值比射速还小
if (fireTimer < fireRate) return;//直接返回跳出函数
//向前射击
//发出射线
fireTimer = 0f;//重置计时器
}
}
7.2 实现打空弹匣
public int bulletsMag = 30;//一个弹匣子弹数量
public int range = 100;//子弹射程
public int bulletsLeft = 300;//剩余备用子弹
public int currentBullets;//当前子弹数
void Start()
{
currentBullets = bulletsMag;
}
public void GunFire()
{
//计时器值比射速还小
if (fireTimer < fireRate||currentBullets<=0) return;//直接返回跳出函数
//向前射击
//发出射线
currentBullets--;//子弹减少
}
7.3 子弹数UI
添加文本(Text)UI
再脚本设置UI
[Header("UI设置")]
public Image crossHairUI;//初始化
public Text AmmoTextUI;
void Start()
{
currentBullets = bulletsMag;
UpdateAmmoUI();//初始化一下
}
/// <summary>
/// 射击
/// </summary>
public void GunFire()
{
currentBullets--;//子弹减少
//每次子弹减少,就调用UI
UpdateAmmoUI();
fireTimer = 0f;//重置计时器
}
public void UpdateAmmoUI()
{
AmmoTextUI.text = currentBullets + "/" + bulletsLeft;
}
7.4 实现换弹装弹
R键换弹
[Header("键位设置")]
[Tooltip("装填子弹的按键")][SerializeField] private KeyCode relodInputKey;//换弹键
public void Relod()
{
if (bulletsLeft <= 0) return;//余弹不足
//计算需要装填的子弹数量=弹匣子弹总数-当前子弹数
int bulletRelod = bulletsMag - currentBullets;
//备弹需要扣除子弹数
int bulletReduce = (bulletsLeft > bulletRelod) ? bulletRelod: bulletsLeft;
bulletsLeft -= bulletReduce;//备弹减少
currentBullets += bulletReduce;//弹匣加装填弹
UpdateAmmoUI();
}
8、添加音效
8.1 添加音源
在Player身上添加音源组件
8.2 行走和跑步音效
在PlayerMove()进行调用
/// <summary>
/// 播放移动音效
/// </summary>
public void PlayMoveAudio()
{
if(isGround&&moveDirection.sqrMagnitude>0.9f)//在地上且在走动
{
audioSource.clip = isRun ? runningAudio : walkingAudio;//走或跑
if(!audioSource.isPlaying)
{
audioSource.Play();//没在播放时播放
}
}
else
{
if(audioSource.isPlaying)
{
audioSource.Pause();
}
}
}
9、开枪特效
9.1 添加开枪粒子特效
添加粒子特效预制体
脚本控制
public ParticleSystem muzzleFlash;//粒子特效
public Light muzzleFlashLight;//灯光
//粒子火花特效
muzzleFlash.Play();
muzzleFlashLight.enabled = true;//启用灯光
9.2 生成弹孔
public GameObject hitParticle;//击中特效
public GameObject bulletHole;//弹孔
//发出射线
if(Physics.Raycast(shootPoint.position,shootDirection,out hit,range))//击中目标信息存入hit
{
Debug.Log(hit.transform.name+"打到惹");
GameObject hitParticleEffect= Instantiate(hitParticle, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));//实例化克隆对象,在被击中位置生成火花特效
GameObject bulletHoleEffect= Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));//实例化克隆对象,在被击中位置生成弹孔
Destroy(hitParticleEffect, 1f);
Destroy(bulletHoleEffect, 3f);//一定时间后销毁
}
10、Player手臂摇晃
public class WeaponSway : MonoBehaviour
{
/*摇摆参数*/
public float amout;//摆动幅度
public float smoothAmout;//摇摆平滑值
public float maxAmout;//最大幅度摇摆
[SerializeField]private Vector3 originPosition;//起始位置
// Start is called before the first frame update
void Start()
{
originPosition = transform.localPosition;//相对于父级物体的变换位置
}
// Update is called once per frame
void Update()
{
//获取鼠标轴值
float mouseX= -Input.GetAxis("Mouse X")*amout;
float mouseY = -Input.GetAxis("Mouse Y") * amout;
//限制
mouseX = Mathf.Clamp(mouseX, -maxAmout, maxAmout);
mouseY = Mathf.Clamp(mouseY, -maxAmout, maxAmout);
//手臂位置变化
Vector3 finallyPosition = new Vector3(mouseX, mouseY, 0);//左右摇晃
transform.localPosition = Vector3.Lerp(transform.localPosition, finallyPosition + originPosition, Time.deltaTime * smoothAmout);
}
}
11、动画
Animator组件:动画状态机,管理动画
项目中新建Animation文件夹并新建Animator Controller组件管理动画
挂载到对应武器的状态机上
11.1 拿出武器、静止、查看武器
1、Exit:全部执行后退出
2、Any Status:任何状态
3、Entry:动画块入口
1. 添加拿出武器动作
2. 添加站立待机动画,对take_out右键make transition指向待机动画块
3. 添加查看武器动画,动画能从待机动画传递到检视武器动画,并且能传递回去
设置进入动画条件:F键检视武器
添加触发器类型参数
触发器设为idle->inspect_weapon的触发条件
[Tooltip("查看武器的按键")] [SerializeField] private KeyCode inspectWeaponKey;//检查武器动画
void Start()
{
anim = GetComponent<Animator>();//获取组件
}
void Update()
{
//检查武器
if(Input.GetKeyDown(inspectWeaponKey))
{
anim.SetTrigger("Inspect");//触发器获取参数
}
}
11.2 行走和奔跑动画
在PlayerMovement脚本
设置动画的触发条件
public bool isWalk;//动画触发条件
void Update(){
//处于行走状态
isWalk = (Mathf.Abs(h_Value) > 0 || Mathf.Abs(v_Value) > 0) ? true : false;//不大于零就说明没在走
}
在WeaponController脚本调用PlayerMovement中的变量
public PlayerMovement PM;
//调用PlayerMovement中变量
anim.SetBool("Run", PM.isRun);//键,值
anim.SetBool("Walk", PM.isWalk);
在Animator添加对应bool参数Run/Walk
11.3 换弹动画
- 弹匣打空换弹
- 弹匣未打空换弹
/// <summary>
/// 播放换弹动画
/// </summary>
public void RelodAnimation()
{
//播放动画1
if(currentBullets>0)
{
anim.Play("reload_ammo_left", 0, 0);//0表示Base Layer
audioSource.clip=relodAmmoLeftClip;
audioSource.Play();
}
if(currentBullets==0&&bulletsMag>0)
{
anim.Play("reload_out_of_ammo", 0, 0);
audioSource.clip = relodOutOfAmmoClip;
audioSource.Play();
}
}
anim.Play()直接播放动画,就不需要画箭头传递,只需要动画播放完毕传回待机状态
处理奔跑、换弹时可以射击的问题
private bool isRelod;//判断是否装弹
AnimatorStateInfo info = anim.GetCurrentAnimatorStateInfo(0);//获取当前第一层动画信息
void Update(){ if(info.IsName("reload_ammo_left")||info.IsName("reload_out_of_ammo"))//获取两个动画名称
{
isRelod = true;//正在换弹
}
else
{
isRelod = false;
}
}
public void GunFire()
{
//计时器值比射速还小
if (fireTimer < fireRate||currentBullets<=0||isRelod||PM.isRun) return;//直接返回跳出函数
}
最终结果:demo文件