飞行和射击
要实现的效果:按住空格可以往上飞
实现飞行
角色的物理特性
在 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
中实现。
- 获取
Configurable Joint
组件。这里获取可以采用SerializeField
,也可以在Start()
函数中使用joint = GetComponent<ConfigurableJoint>();
获取 GetComponent
会在当前物体中寻找组件,找不到会报错,有多个会随机选一个- 在按下空格时将
Configurable Joint
的yDrive
变为0,松开空格时变回原值
限制一下视角角度
限制一下角色向上和向下看的角度。在 PlayerController
中实现
- 新建一个变量记录摄像头在x轴上转到了多少角度
- 将该角度限制在一个范围中:
Mathf.Clamp(cameraRotationTotal, -cameraRotationLimit, cameraRotationLimit);
, 限制在了[-cameraRotationLimit, cameraRotationLimit]
中 - 将旋转角度直接赋值给物体的
transform
:cam.transform.localEulerAngles = new Vector3(cameraRotationTotal, 0f, 0f);
实现射击
在 Player
中添加脚本 PlayerShooting.cs
- 在
Update()
中获取用户输入 (鼠标左键射击,名字是Fire1
。)这里ctrl
也会触发射击,但未来我们会用ctrl
做下蹲,所以删掉这里先设定枪为半自动,鼠标左键按下后只发出一发子弹。
- 实现一个射击函数
Shoot()
,每按下左键就开始射击
射击函数编写
射击涉及的属性:伤害值、射击距离、枪械名称。这些属性归属于同一把枪,我们希望这些属性可以放在一起管理,因此我们创建一个脚本 PlayerWeapon.cs
武器管理
PlayerWeapon
是一个普通的类,不需要继承基类 MonoBehaviour
。类中包含以下三个属性
- 武器名称
- 伤害值
- 射程
为了能在unity编辑器中编辑类的属性值,在类上添加注解 [Serializable]
,并将属性变为 public
回到 PlayerShooting
中,加入 PlayerWeapon
类,添加 [SerializeField]
,这时可以在右边看到武器属性
射击逻辑
这里设定游戏的射击逻辑为从玩家摄像头(未来会是准心)发出一条射程长度的线,线上的物体被判定为击中。
实现逻辑:
- 获取摄像头位置
- 由于摄像头在
Player
的子组件里,因此需要用GetComponentInChildren
- 调用unity封装好的函数,从摄像头位置发出一个射线,射线会返回第一个碰到的物体
- 由于游戏中不开启友伤时队友和敌人需要区分,因此可以加一个额外参数
mask
,射线会返回击中的第一个mask
层的物体。未来队友和敌人也会分层,以此来区分。目前还没有添加敌友,因此mask
选择Everything
给 Player
赋名字
不同 Player
需要有一个不同的名字,方便以后调试。这里使用下面的代码赋值:
transform.name = "Player " + GetComponent<NetworkObject>().NetworkObjectId;
: transform.name
是物体的名字,GetComponent<NetworkObject>().NetworkObjectId
是调用标识网络物体的 NetworkObject
的API,它会将联机的所有玩家从1开始编号,保证所有玩家的名字在所有对局窗口中都相同
联机射击通信
如何让一个玩家的射击行为显示在所有玩家的对局中?按照我们上一次讲的Client模式,玩家射击后,将射击信息传递给Server,Server广播给其他所有玩家。
要实现这一个模式,需要用到ServerRpc,这个注解的作用是将函数发送到Server端执行,因此这个发送到Server端的函数可以起到传递信息的作用
要使用该注解,需要注意以下几点
- 发送到Server端的函数一定要以
ServerRpc
结尾 - 函数所在的类一定要继承
NetworkBehaviour
,因为这是一个网络行为 - 这里在
Shoot()
函数中,击中并返回物体后,将后续的处理放进ShootServerRpc()
中并调用。
在射击行为中,会出现一个上次编写联机出现过的问题:本地玩家可以操控联机的玩家,因为所有角色都会从本地(所有主机)读取用户操作,因此要把 PlayerShooting
组件disable掉。
在游戏画面中输出信息
在 Scene
中创建一个空物体 GameManager
, 为 GameManager
添加一个脚本 GameManager.cs
,脚本代码见文章下方
阶段成品
修改的代码
PlayerInput.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerInput : MonoBehaviour
{
[SerializeField]
private float speed = 5f; // 加上SerializeField注解,可以在unity里调整speed的值
[SerializeField]
private float lookSensitivity = 50f; // 鼠标灵敏度
[SerializeField]
private float thrusterForce = 20f; // 角色向上推力
[SerializeField]
private PlayerController controller;
private ConfigurableJoint joint;
// Start is called before the first frame update
void Start()
{
Cursor.lockState = CursorLockMode.Locked;
joint = GetComponent<ConfigurableJoint>();
}
// Update is called once per frame
void Update()
{
float xMove = Input.GetAxisRaw("Horizontal"); // 获取用户在x轴上的移动方向,1表示右,-1表示左,0表示不动
float zMove = Input.GetAxisRaw("Vertical"); // 获取用户在z轴上的移动方向,1表示前,-1表示后,0表示不动
Vector3 velocity = (transform.right * xMove + transform.forward * zMove).normalized * speed;
controller.Move(velocity); // 将速度传递给PlayerController来实现运动
// 获取鼠标移动数据
float xMouse = Input.GetAxisRaw("Mouse X"); // 鼠标水平移动
float yMouse = Input.GetAxisRaw("Mouse Y"); // 鼠标垂直移动
// Debug.Log(xMouse.ToString() + " " + yMouse.ToString()); // 调试信息
Vector3 yRotation = new Vector3(0f, xMouse, 0f) * lookSensitivity; // 角色y轴旋转向量
Vector3 xRotation = new Vector3(-yMouse, 0f, 0f) * lookSensitivity; // 角色x轴旋转向量
controller.Rotate(yRotation, xRotation);
Vector3 force = Vector3.zero; // 力是一个三维向量,初始为0
if (Input.GetButton("Jump"))
{
force = Vector3.up * thrusterForce;
joint.yDrive = new JointDrive
{
positionSpring = 0f,
positionDamper = 0f,
maximumForce = 0f,
};
}
else
{
joint.yDrive = new JointDrive
{
positionSpring = 20f,
positionDamper = 0f,
maximumForce = 40f,
};
}
controller.Thrust(force);
}
}
PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private Rigidbody rb; // 加上注解后在unity里将要绑定的rigidbody拖进变量中
[SerializeField]
private Camera cam; // 要操作视角,需要摄像头
private Vector3 velocity = Vector3.zero; // 速度:每秒移动的距离,初始为0(一定要清楚速度对应的时间单位是帧还是秒)
private Vector3 yRotation = Vector3.zero; // 旋转角色
private Vector3 xRotation = Vector3.zero; // 旋转视角
private Vector3 thrusterForce = Vector3.zero; // 向上的推力
private float cameraRotationTotal = 0f; // 累计转了多少度
[SerializeField]
private float cameraRotationLimit = 85f;
public void Move(Vector3 _velocity)
{
velocity = _velocity; // 将用户输入的速度向量传过来
}
public void Rotate(Vector3 _yRotation, Vector3 _xRotation) // 将用户输入的旋转向量传过来
{
yRotation = _yRotation;
xRotation = _xRotation;
}
public void Thrust(Vector3 _thrusterForce)
{
thrusterForce = _thrusterForce;
}
private void PerformMovement()
{
if (velocity != Vector3.zero) // 这里是一个优化,只有当角色有速度时才进行移动操作
{
rb.MovePosition(rb.position + velocity * Time.fixedDeltaTime);
}
if (thrusterForce != Vector3.zero)
{
rb.AddForce(thrusterForce); // 作用Time.fixedDeltaTime秒:0.02s
}
}
private void PerformRotation()
{
if (yRotation != Vector3.zero)
{
rb.transform.Rotate(yRotation); // 旋转刚体
}
if (xRotation != Vector3.zero)
{
cameraRotationTotal += xRotation.x;
cameraRotationTotal = Mathf.Clamp(cameraRotationTotal, -cameraRotationLimit, cameraRotationLimit);
cam.transform.localEulerAngles = new Vector3(cameraRotationTotal, 0f, 0f); // 不旋转刚体,只旋转摄像头
}
}
private void FixedUpdate() // 每秒中以固定时间间隔执行固定次数
{
PerformMovement();
PerformRotation();
}
}
PlayerSetup.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
public class PlayerSetup : NetworkBehaviour
{
[SerializeField]
private Behaviour[] componentsToDisable;
[SerializeField]
private Camera sceneCamera;
// Start is called before the first frame update
void Start()
{
if (!IsLocalPlayer)
{
DisableComponents();
}
else
{
sceneCamera = Camera.main;
if (sceneCamera != null)
{
sceneCamera.gameObject.SetActive(false);
}
}
SetPlayerName();
}
private void SetPlayerName()
{
transform.name = "Player " + GetComponent<NetworkObject>().NetworkObjectId;
}
private void DisableComponents()
{
for (int i = 0; i < componentsToDisable.Length; i++)
{
componentsToDisable[i].enabled = false;
}
}
private void OnDisable()
{
if (sceneCamera != null)
{
sceneCamera.gameObject.SetActive(true);
}
}
}
PlayerShooting.cs
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
public class PlayerShooting : NetworkBehaviour
{
[SerializeField]
private PlayerWeapon weapon;
[SerializeField]
private LayerMask mask;
private Camera cam; // 角色摄像头
// Start is called before the first frame update
void Start()
{
cam = GetComponentInChildren<Camera>();
}
// Update is called once per frame
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);
}
}
PlayerWeapon.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class PlayerWeapon
{
public string name = "AUGA3";
public int damage = 10;
public float range = 100f;
}
GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
private static string info;
public static void UpdateInfo(string _info)
{
info = _info; // 更新信息
}
private void OnGUI()
{
GUILayout.BeginArea(new Rect(200f, 200f, 200f, 400f)); // 创建一个矩形区域来显示信息
GUILayout.BeginVertical(); // 信息垂直排列
GUILayout.Label(info); // 显示信息
// 结束区域绘画
GUILayout.EndVertical();
GUILayout.EndArea();
}
}
著作权信息
- 源视频作者:yxc
- 文章作者: jaylenwanghitsz
- 链接:https://www.acwing.com/file_system/file/content/whole/index/content/8140263/
- 来源:AcWing
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。