射击demo

Day 2025/3/1

FPS射击第一人称射击demo

 

玩家移动

    void Update()

    {

        speed = walkSpeed;

        float h = Input.GetAxisRaw("Horizontal");

        float v = Input.GetAxisRaw("Vertical");

        direction = (transform.right*h+transform.forward*v).normalized;

        characterController.Move(direction*speed*Time.deltaTime);

    }

视野旋转

上下挪动相机的上下轴

 

Transform.localRotation = qua.euler(moveX,0,0);

相机的上下默认是反转的,并且要限制上下可转动的值

所以是

Movex -= movex

MoveX = mathf.clampmoveX-60,60

        RotateY -= mouseY;

        RotateY = Mathf.Clamp(RotateY,-60f,60f);

        transform.localRotation = Quaternion.Euler(RotateY,0f,0f);

这里犯了一个错误就是直接用mouseY移动镜头,但起始每次update都会重置mouseY,所以实际上要累加到rotateY中

 

 

 

视野旋转的左右挪动玩家自身左右轴

Vector3的基本函数

Vector3.right == 1,0,0

Vector3.up== 0,1,0

所以是

playerTransform.rotatevector3.up*mouse.x

奔跑和行走的判断

奔跑用shitf   input.getkey判断

行走用水平和垂直是否为0判断

玩家跳跃

一个isJumpisGround的判定

按下空格后获得一个升空有效值并且isJump = true

玩家控制器会用升空值每次进行累加+并且每次执行升空值会有衰减

 

衰减值也要设置比较大的

Fallforce = 10f

 

升空值衰减到负数会下降,碰到地面后isjump = false

升空值归零

 

 

Day 2025/3/2

蹲下

几个判断变量

当前是否在蹲下

当前是否可蹲下

蹲下时高度

站立时高度

 

逻辑:获取任务头部位置(站立)

1然后在头部生成一个检测范围,检测到头部有物体则isCanCrouch = false

 

  1. 根据按ctrl和松开控制角色高度和中心位置,当在隧道时(检测头顶有东西),返回crouch

        // 相机随人物蹲起移动0.9偏移

        float targetHeight = characterController.height*0.9f;

        // 相机设置平滑值

        cameraHeight = Mathf.Lerp(cameraHeight,targetHeight,12f*Time.deltaTime);

        transform.localPosition = Vector3.up*cameraHeight;

       

 

    private void checkHead(){

        // 获取头部位置并在头部生成一个检测范围

        Vector3 headLocation = transform.position+Vector3.up*standHeight+new Vector3(0,0.3f,0);

        // Physics.OverlapSphere(生成位置,检测范围,检测层)

        hasObs = Physics.OverlapSphere(headLocation,characterController.radius,crouchLayerMask).Length>0?true:false;

    }

    public void crouch(){

        if (hasObs)

        {

            return;

        }

 

        if (Input.GetKey(KeyCode.LeftControl))

        {

            isCrouch = true;

            characterController.height = crouchHeight;

        }else

        {

            isCrouch = false;

            characterController.height = standHeight;

        }

        characterController.center =new Vector3(0,characterController.height/2,0);

    }

音效播放

玩家正在移动时播放音效

引入枪械和相机

创建自动步枪和动画状态机

连线操作:

ExitTime:播放xx进度后转换

Fixduration:转换时间

枪械脚本,添加特效

抽象类

父类

class+abstract

子类

去除monobehaviour继承,继承wepon方法,右键实现抽象类

然后在父类中写常用方法

public abstract class weapon : MonoBehaviour

{

    public abstract void fire();

    public abstract void reload();

    public abstract void reloadAnimation();

    public abstract void expaningCrossUpdate(float expanDegree);

    public abstract void aimIn();

    public abstract void aimOut();

}

 

射线检测:

Raycasthit hit

Physics.raycast(起始位置, 射出方向,out hit输出信息,射线最大距离)

枪有后坐力,射出方向添加偏移值

转换成世界坐标,然后xy轴随机数

Day 2025/3/5

控制射速

比较简单,就是计时器累加执行fire函数

打空弹匣

三个参数

单弹匣子弹数

当前子弹数

备弹数

开火粒子特效和灯光效果

Light 灯光

灯光持续时间

PracticalSystem两个粒子特效

最小粒子数1

最大粒子数7

 

使用协程函数-灯光打开后0.02s关闭

协程的使用方法

Private IEnumerator void lightOn(){

Lght.enable = true

Yield return new waitforsecond(waitTime);

Light.enable = false;

}

 

调用

StartcoroutinelightOn())

 

然后发射粒子特效个数

Muzzlepatic.emitrandom

开火音效

开火音效

带消音开火音效

换子弹

空弹匣换子弹

开镜

不同的枪械音效不同,所以要设置内部类

内部类不可以直接访问,要加上序列化

[System serializable]

Public class soudClips{

}

不知道是什么原因我的音效类序列化后并没有显示在面板上,所以就不用序列化了

准星

准星UI(这里视频里用的是img数组形式,不过我感觉可以直接引入准星,后坐力直接与准星大小正正比)

视频中用的是每次射击移动4个准星的位置,更新当前准星,使用math.clamp限制准星大小

 

当前准星开合度curr

默认准星开合度cross

最大开合度max

弹药数

射击模式

当前玩家行动状态

 

(可选)

使用协程每帧执行5次准星变大

IEnumerator crs(){

Yeild return null;

For(i<0;i=5;i++){

crossHit(time.detaltime *500 )

}

}

准星更新函数:传入目标准星,小于则逐步扩大,大于则逐步缩小

Crossupdate(num){

If(curr<num-5){

Cross(150*time.detaltime)

}elseif(curr>num+5){

Cross(-150*time.detaltime)

}

} //目标准星值要要划出一个范围,因为准星累加和累减不是整数,会反复跳动

 

这样写的好处是每次射击后扩大准星,但是会根据当前人物行动状态弹回

 

 

Day 2025/3/6

子弹和弹孔特效

 

子弹

子弹抛壳

 

Rigid body.velocity:向刚体设置速度

子弹向前射出的同时添加shootPointDirection后坐力范畴)*子弹力度

换子弹逻辑和子弹UI

子弹文本

射击模式文本

 

换弹:备弹小于等于零,返回

需要填充子弹量:弹夹上限-当前子弹,备弹小于需要补充子弹-填充剩下备弹

当前子弹数是满的不用换弹

Day 2025/3/7

添加开火动画

添加开火状态淡入淡出效果0.1s

Animator.crossfadeinfixedTime(fire,0.1f)

 

当前执行取出枪械动画不可射击

If(Animator.getCurrentAnimatorStateInfo(0).isName(take_out))

半自动和全自动

引入靶子

射击模式切换UI 这个直接修改就好了,就不用做方法了

CheckWeapon

射击模式枚举值

是否是全自动武器

模式切换的中间参数1,2,3

布尔值input

 

Public enum shootMode{

auto,

semi

}

 

按下切换中间值,文本,shootingmode

枚举值Switch

 

简单来说就是全自动inputMode =1,半自动inputMode = 2

然后switch 12

进入1时一直按下左键 input就为true

半自动时修改射速为0.2

 

开火根据input值判断开火

换弹动画

Animator.play(“”,0,0)

引入第二个audio Source

动画播完后再换弹

换弹时不能射击

行走,奔跑,检查武器动画

Animator.setbool(run,player.isRun)

F键检查武器

Animator.settrigger

 

Bug:多次按F会在结束动画后延迟多次执行检视动画

解决:在inpect动画中添加行为:清除信号量

Public string[] clearAtEnter

Public string[] clearAtExit

onstateExit(){

For i<clearatExit.length

Animator.resettrigger(clearatExit[i]) //表示清除信号

}

添加瞄准

是否正在瞄准

主摄像机

枪摄像机

瞄准扩大视野

 

 

Setbool aim = true

调整枪械位置(这里我感觉可以不移动枪械位置,中心点不变的就行,没有太大影响)

 

以下视频中根据播放进度执行,这边不浪费时间就按下直接执行了

Camera.filedOfView = mathf.smooth(30,60,平滑值,转换时间)

隐藏准星

播放开镜音效

开镜与腰射子弹偏移值

后坐力

Day 2025/3/9

消音武器

瞄准镜

特效

脚本-是否装备消音器-切换开枪音效

Day 2025/3/10

切换武器

当前武器编号

Input.getaxis(“mouse scrollwheel”) //鼠标滚轮

如果切换的枪超出数组范围则切回第一把,小于长度则切回最后一把

先获取所有武器下标

Ayyay.indexOf

然后取最大值

Array.Max()

 

拾取武器和丢武器

 

 

    // 拾取

    private void addWeapon(GameObject gameObject){

        // 假设现在武器仓只能拿3把,武器满了不可拾取

        if (inven.Count>3 || inven.Contains(gameObject))

        {

            return;

        }

 

        // 拾取之后切换到最后一把(新)

        inven.Add(gameObject);

        switchWeapon(inven.Count-1);

    }

    // 丢弃

    private void removeWeapon(GameObject weapon){

        // 当前没有武器不可丢弃

        if (inven.Count == 0)

        {

            return;

        }

        // 删除,隐藏,切换

        switchWeapon(currWeapon+1);

        inven.Remove(weapon);

    }

 

手枪

day 2025/3/11

散弹枪,狙击枪

散弹枪3个换弹状态

Reload_open,reload_insert,reload_close

Has_exittime = false

Conditions - shotGun_reload = true

6reload_insert

 

1次打出的子弹数 = 8

循环8次射线检测

 

判断是否是散弹枪生成8颗子弹

If(){}

Else{

Bullet = instaniate(bulletprefab,hit.point,quaternion.fromToRotation(vector3.up,hit.normal))

}

 

动态改变打出的子弹数:只要gameobject.name == 4的就射出8

 

狙击镜瞄准时颜色

没瞄准时颜色

狙击镜材质

狙击镜开瞄准时视野为15

Day 2025/3/12

可拾取物品

PickUp节点

添加物品组件-两倍缩放-60

添加boxCollider

 

武器旋转速度

Transform.eulerangles += vector3 0speed*time.detaltime0

武器编号

武器gameobject

 

If enterCollider

 

Inventory中添加该枪械到数组,如果已有枪械则重置备弹

 

Player中调用inventoryadd方法

If inventory.wepons.containsweaponreturn

添加玩家血量

玩家生命值

是否死亡

是否收到伤害

更新生命值

Player.health-=damage

Isdamage = true

PlayerhealthUI.text =

If health<0 player.isdead = true;playerhealthUI = “死亡”

Timescale = 0

敌人AI

敌人模型:mixamo.com

下载模型

了解导航系统并设置3条巡逻路线

Nav mesh agent网格系统

 

 

Enemy.cs

引入Animator

引入Nav mesh agent网格系统

不同敌人不同路线

Gameobject[] wayPoint

Lsit<vector3>

当前状态

加载路线void(game object)

敌人向目标点前进void moveTarget{

Vector3.moveTowards(position,targetTrans)

}

 

wayPoint空节点-3个点

 

EnemybaseState.cs

AttackState.cs  继承basestate

Patrolstate.cs  继承basestate

Day 2025/3/14

很多技术不错的程序员都喜欢用一些复杂的代码写简单的功能,不可否认这样的代码看起来更严谨代码报错率也更低,但是这样真的会令我这种新手看起来晕乎乎的

比如这段:

Up主限制了index的范围

Mathf.clamp(enemy.index,0,enemy.waypoint.count-1)

又从物理层面上设置了敌人到达最后一个导航点后下标归零的操作

If vector3.distance(enemy.transform.position,enemy.waypoint[enemy.count-1])<0.5f{

Index = 0;

}

而我只想到了下标自增到最后一个导航点就归零

If index == waypoint.count{ index = 0}

 

Vector3.moveToWards()  //朝目标点移动,返回移动后的距离

Vector3.moveToWards(起始位置,目标位置,移动距离) 

然后再调用角色导航组件navmeshagentdestination角色会朝目标点平滑移动

 

 

将公共抽象类中的主方法持续执行并传入敌人脚本,使子类可以调用敌人脚本

BaseState.cs

public abstract void onUpdate(enemy enemy);     //持续执行

 

Enemy.cs

void Update()

    {  

        // 当前状态持续执行

        currentState.onUpdate(this);

    }

 

到达导航点后敌人会站立一段时间

// 等待站立状态结束后继续行走

        if (enemy.animState == 0)

        {

            timer += Time.deltaTime;

             If(timer>=standTime){ timer = 0;enemy.animState = 1; }

        }

        If(enemy.animState == 1){

       moveToTarget()

       

}

多个敌人随机路线的思路

一个路线管理器

将所有路线id打乱

        // raw是按序分配的路线,using是打乱的路线

        // 随机,添加,删除

        for (int i = 0; i < rawIndex.Count; i++)  //这里循环是固定的,会循环完整个数组

        {

            int rIndex = Random.Range(0,rawIndex.Count); //这里数组cout是动态的,每次删除元素后只会随机剩下的元素

            usingIndex.Add(rIndex);

            rawIndex.RemoveAt(i);

        }

敌人的id是固定的

加载路线时按敌人顺序分配打乱的路线id

制作敌人血条并添加血条逻辑

敌人血量float

敌人血条UI

受到伤害UI

敌人死亡特效gameobject

是否死亡

Health方法传入damage

死亡返回

更新收到伤害

更新血量

更新血条UI

If health<0 die animator.settriger(dying)

 

枪射线判断

If hit == enemy

调用health方法,传入随机数伤害

 

每把枪设置minDamagemaxDamage

 

给敌人加上碰撞组件boxcollider

 

敌人脚本死亡后停止更新

敌人攻击状态切换

List<transform> 敌人检测范围

攻击间隔

攻击阶段时间

攻击距离

攻击特效

攻击位置

攻击声音

播放器

 

Entertrigger

If !isdead && !list.conttains && tag !=bullet

attackList.add

 

Exittrgger

Attack.remove

 

Move方法

If list.cout == 0  targetPos =  Vector3.MoveTowards(transform.position,wayPoint[index],agent.speed*Time.deltaTime);

 

Else

targetPos =  Vector3.MoveTowards(transform.position,list[0].transform.position,agent.speed*Time.deltaTime);

 

onupdate方法

If list.count>0

Enemy.transitiontoState(enemy.attackstate)

 

Animator-攻击层

Day 2025/3/15

攻击状态

Attack.cs

在攻击状态中的三个判断

没有敌人-切换回巡逻状态

有多个敌人-寻找距离最近的敌人

1个敌人-攻击敌人

最后if targetpoint.tag == player{}

Enemy.moveToTarget()

Day 2025/3/16

玩家血雾效果

Image 玩家血雾

Color 红色

Color.clear清空色

 

制作敌人攻击音效

 

玩家死亡后两个播放器暂停声音

 

完成,最终效果展示

 

 

posted @ 2025-03-16 19:46  无法言语的寂灭之形  阅读(33)  评论(0)    收藏  举报