Unity学习-实现飞行和射击


飞行和射击

要实现的效果:按住空格可以往上飞

mind map

实现飞行

角色的物理特性

在 Player 中的 Rigidbody 组件负责 Player 的物理特性。我们的角色目前是一个球体,但我们不希望角色像球体一样, 被碰到后会螺旋升天,所以我们先禁掉球体的旋转特性。

添加弹簧效果

这里为了实现飞行和下落,我们采用给角色添加弹簧效果的方式。

在 Player 中添加组件 Configurable Joint

我们的飞行和下落是角色在y轴上的移动,因此只需要y轴上添加弹簧效果

  • Position Spring: 扭矩,跟弹力成正比,这里设成20
  • Position Damper 不变
  • Maximum Force:最大弹力,原数值太大,物体再做简谐运动时有可能会穿模; 这个力也不能太小,否则下落会比较慢,因此将弹力改成40

目前角色可以在y轴上做简谐运动,但持续时间比较长,可以增加角色的摩擦力来加快静止

添加脚本

添加脚本,当我们按下空格时持续给角色一个力让它向上飞,松开空格时角色下落。

空格在unity中对应的名称是 Jump

控制角色跳跃的逻辑类似之前控制角色移动的逻辑

  • PlayerInput 中获取用户输入,产生力的向量

  • 获取用户输入时 GetButton 获取用户是否按住某个键;GetButtonDown 获取用户是否有按下过某个键

  • 将力的向量传递给 PlayerController

  • 将力施加给角色:rb.AddForce(thrusterForce); , 默认施加时间为0.02s

目前设定的推力是20,弹力最大是40,因此角色无法飞起来。为了让角色飞起来,同时不让角色下落太慢,我们可以在跳跃时取消弹力限制,下落时还原。在 PlayerInput 中实现。

//Playerinput 
Vector3 force = Vector3.zero;
        if (Input.GetButton("Jump"))//获取到空格键
        {
            force = Vector3.up * thrusterForce;//向上的分量获得推力
            joint.yDrive = new JointDrive//为整个Configurable Joint结构体赋值
            {
                positionSpring = 0f,
                positionDamper = 0f,
                maximumForce = 0f,
            };
        }
        else//松开空格,恢复弹力
        {
            joint.yDrive = new JointDrive
            {
                positionSpring = 20f,
                positionDamper = 0f,
                maximumForce = 40f,
            };
        }
        controller.Thrust(force);//将弹力传递到controller类
  • 获取 Configurable Joint 组件。这里获取可以采用 SerializeField ,也可以在 Start() 函数中使用 joint = GetComponent();
  • 获取GetComponent 会在当前物体中寻找组件,找不到会报错,有多个会随机选一个
  • 在按下空格时将 Configurable Joint 的 yDrive 变为0,松开空格时变回原值

限制一下视角角度(视角不能上下360旋转)

限制一下角色向上和向下看的角度。在 PlayerController 中实现

  • 新建一个变量记录摄像头在x轴上转到了多少角度
  • 将该角度限制在一个范围中:Mathf.Clamp(cameraRotationTotal, -cameraRotationLimit, cameraRotationLimit); , 限制在了 [-cameraRotationLimit, cameraRotationLimit] 中
  • 将旋转角度直接赋值给物体的 transform : cam.transform.localEulerAngles = new Vector3(cameraRotationTotal, 0f, 0f);

cam.transform.localEulerAngles和cam.transform.Rotate;区别:
localEulerAngles 直接设置对象的局部欧拉角旋转,而 Rotate 方法对当前旋转进行增量旋转。
使用 localEulerAngles 可能更直观和简单,特别是当你只想设置对象的特定旋转角度时。
使用 Rotate 方法可以更灵活地处理增量旋转,对于动态或交互性的旋转效果更为方便。

if (xRotation != Vector3.zero)
       {
          cameraRotationTotal += xRotation.x; //向量的 x 分量
	  cameraRotationTotal = Mathf.Clamp(cameraRotationTotal, -cameraRotationLimit, cameraRotationLimit);// Mathf.Clamp 方法将 cameraRotationTotal 限制在 -cameraRotationLimit 和 cameraRotationLimit 之间
	  cam.transform.localEulerAngles = new Vector3(cameraRotationTotal, 0, 0);
       }

实现射击

在 Player 中添加脚本 PlayerShooting.cs

1、在 Update() 中获取用户输入 (鼠标左键射击,名字是 Fire1。)这里 ctrl 也会触发射击,但未来我们会用 ctrl 做下蹲,所以删掉

这里先设定枪为半自动,鼠标左键按下后只发出一发子弹

2、实现一个射击函数 Shoot() ,每按下左键就开始射击

射击函数编写

射击涉及的属性:伤害值、射击距离、枪械名称。这些属性归属于同一把枪,我们希望这些属性可以放在一起管理,因此我们创建一个脚本 PlayerWeapon.cs

武器管理

PlayerWeapon 是一个普通的类,不需要继承基类 MonoBehaviour 。类中包含以下三个属性

  • 武器名称
  • 伤害值
  • 射程

为了能在unity编辑器中编辑类的属性值,在类上添加注解 [Serializable] ,并将属性变为 public

回到 PlayerShooting 中,加入 PlayerWeapon 类,添加 [SerializeField] ,这时可以在右边看到武器属性

射击逻辑

这里设定游戏的射击逻辑为从玩家摄像头(未来会是准心)发出一条射程长度的线,线上的物体被判定为击中。

实现逻辑:

  • 获取摄像头位置
  • 由于摄像头在 Player 的子组件里,因此需要用 GetComponentInChildren
  • 调用unity封装好的函数,从摄像头位置发出一个射线,射线会返回第一个碰到的物体
  • 由于游戏中不开启友伤时队友和敌人需要区分,因此可以加一个额外参数 mask ,射线会返回击中的第一个 mask 层的物体。未来队友和敌人也会分层,以此来区分。目前还没有添加敌友,因此 mask 选择 Everything
private Camera cam; //角色摄像头
// Start is called before the first frame update
void Start()
{
    cam = GetComponentInChildren<Camera>(); //与GetComponent<>()相比,这个还可以获取子对象的组件
}

void Update()
    {
        if (Input.GetButtonDown("Fire1"))
        {
            Shoot();
        }
    }

    private void Shoot()
    {
        RaycastHit hit;
        if (Physics.Raycast(cam.transform.position, cam.transform.forward, out hit, weapon.range, mask))
        {
            ShootServerRpc(hit.collider.name);
        }
    }

    [ServerRpc]
    private void ShootServerRpc(string hittedName)
    {
        GameManager.UpdateInfo(transform.name + " hit " + hittedName);
    }
  1. 代码中的 Input.GetButtonDown("Fire1") 是用于检测是否按下了名为 “Fire1” 的输入按钮(通常是鼠标左键或触摸屏幕)。在 Update() 函数中,如果检测到按下了该按钮,将调用 Shoot() 方法。
  2. Shoot() 方法中的 Physics.Raycast() 用于发射一条射线,从摄像机(cam)的位置沿着摄像机的前方方向,并检测是否与指定的图层(mask)上的物体发生了碰撞。如果射线与物体发生了碰撞,将触发 ShootServerRpc() 方法,并将碰撞到的物体的名称作为参数传递给该方法。
  3. ShootServerRpc() 方法被标记为 [ServerRpc],这表示它是一个服务器远程过程调用方法。该方法会向服务器发送一条消息,通知服务器发生了射击,并传递了被击中物体的名称。
  4. ShootServerRpc() 方法中,调用了 GameManager.UpdateInfo() 方法,并将发射射线的物体的名称和被击中物体的名称作为参数传递给该方法。这里假设 GameManager 是一个游戏管理器类的实例,UpdateInfo() 方法用于更新游戏中的信息。

给 Player 赋名字

不同 Player 需要有一个不同的名字,方便以后调试。这里使用下面的代码赋值:

transform.name = “Player “ + GetComponent().NetworkObjectId; : transform.name 是物体的名字,GetComponent().NetworkObjectId 是调用标识网络物体的 NetworkObject 的API,它会将联机的所有玩家从1开始编号,保证所有玩家的名字在所有对局窗口中都相同

在PlayerSetup.cs中添加下列代码:

//..
void Start()
{
    //...
    SetPlayerName();
}
//.....

//.....
private void SetPlayerName()
{
    transform.name = "Player" + GetComponent<NetworkObject>().NetworkObjectId;
}
//..

联机射击通信

如何让一个玩家的射击行为显示在所有玩家的对局中?按照我们上一次讲的Client模式,玩家射击后,将射击信息传递给Server,Server广播给其他所有玩家。

要实现这一个模式,需要用到ServerRpc,这个注解的作用是将函数发送到Server端执行,因此这个发送到Server端的函数可以起到传递信息的作用

[ServerRpc]
private void Shoot2ServerRpc(string hittedname)
{
	Debug.Log(transform.name + "hit" + hittedname);
}

需要注意的是在server端(也有所有的代码)没有本地用户这一概念,也就是说shootServerRpc等都被禁用掉了,也就执行了,这就产生了矛盾,此时我们可以换一种理解方式,就是相当于用户将这个代码发送给服务端,让服务端来帮忙运那些代码

要使用该注解,需要注意以下几点

1、发送到Server端的函数一定要以 ServerRpc 结尾
2、函数所在的类一定要继承 NetworkBehaviour ,因为这是一个网络行为
3、这里在 Shoot() 函数中,击中并返回物体后,将后续的处理放进 ShootServerRpc() 中并调用。

在射击行为中,会出现一个上次编写联机出现过的问题:本地玩家可以操控联机的玩家,因为所有角色都会从本地(所有主机)读取用户操作,因此要把 PlayerShooting 组件disable掉

在游戏画面中输出信息

在 Scene 中创建一个空物体 GameManager , 为 GameManager 添加一个脚本 GameManager.cs,脚本代码见文章下方


Author: 寒风渐微凉
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source 寒风渐微凉 !
 Previous
Next 
  TOC