从零开始做一款Unity3D游戏<二>——移动,相机控制与碰撞

news2024/12/23 6:18:10

移动玩家

玩家对象的创建

理解向量

获取玩家输入

相机跟随

使用Unity的物理系统

刚体运动

碰撞体和碰撞

使用碰撞体触发器

总结


本文主要来自<<C#实践入门>>哈里森.费隆 著,仅用为做笔记。

当玩家开始玩一款新的游戏时,要做的第一件事就是熟悉角色移动和相机控制。这是一件很有意思的事情,可以让玩家对游戏玩法有一定的预期。Hero Bor 游戏中的角色将是一个胶囊,可以使用键盘上的 W、A、S、D 或方向键进行移动和旋转。在本章,你将首先学习如何通过操纵对象的 Transform 组件来进行移动,然后了解如何通过对物体施加力来控制移动,使运动效果更真实。当移动玩家时,相机会从稍微靠近后方及上方的位置进行跟随,从而使实现瞄准这一射击机制更加容易。最后,本章将使用之前制作的可拾取道具来探索 Unity 的物理系统如何处理碰撞与交互。

本章包含以下内容:

  • 移动与旋转变换。
  • 管理玩家输入。
  • 编写相机行为脚本。
  • Unity 的物理系统和力的应用
  • 基本的碰撞体与碰撞检测。

移动玩家

当决定以何种最佳方式在虚拟世界中移动玩家时,需要考虑如何才能使移动看起来最真实,并避免昂贵的计算开销。在大部分情况下,我们都需要采用折中方案,Unity也不例外。

移动游戏对象的三种常见方式及结果如下。

  • 使用游戏对象的Transform 组件来进行移动和旋转。这是最简单的方式,也是我们的首选方式。
  • 为游戏对象附加 Rigidbody 组件并在代码中施加力。这种方式需要依赖Unity的物理系统来处理繁重的工作,从而提供更真实的效果。本章后续部分会修改代码以使用这种方式。

注意:Unity 建议在移动或旋转游戏对象时保持一致的方式。你可以操纵对象的Transform或RigidBody组件,但不要同时使用它们两者

  • 附加现有的 Unity 组件或预制体,例如CharacterController或FirstPersonController。这样可以减少样板代码,并且仍然能够提供真实的效果,另外还缩短了原型制作时间。

玩家对象的创建

我们想要把 Hero Born 设计为第三人称冒险游戏,这个游戏的起点就是一个可以通过键盘输入进行控制的胶囊以及跟随这个胶囊进行移动的相机。即使这两个对象在游戏中可以一起工作,但为了方便控制,我们还是将它们分开为好。

实践:创建胶囊

只需要执行如下步骤,即可创建出用来表示玩家的胶囊。

(1)在 Hierarchy 面板中使用 Create|3D Object|Capsule创建一个新的 Capsule对象,命名为Player。

(2)选择 Player 对象,单击 Inspector 面板底部的 Add Component 按钮。搜索Rigidbody 并按Enter 键,将 Rigidbody 组件添加到Player 对象上。

(3)展开Rigidbody组件底部的 Constraint 属性并选中 Freeze Rotation 中的x轴和y轴。

 刚刚发生了什么

我们使用Capsule图形、Rigidbody 组件和亮绿色材质创建了 Player 对象。不必疑感Rigidbody 组件到底是什么,现在只需要知道 Rigdbody组件能使 Player 对象与物理系统进行交互即可。具体细节我们将在讨论 Unity 的物体系统时进行说明。

理解向量

现在已经创建好了 Player 对象和相机,可以开始了解如何通过Transform组件来移动和旋转游戏对象了。Translate 和 Rotate 方法属于 Unity 提供的 Transform类,它们都需要使用一个向量作为参数。

在Unity 中,向量用来保存2D或3D空间中的位置和方向,因而存在两种变量:Vector2和 Vector3。这两种变量能够像任何其他变量那样使用,只不过它们代表不同的信息。因为我们的游戏是三维的,所以需要使用 Vector3,这意味着你需要知道x从2的值。对于2D 向量,则只需要知道x 和y的值。记住,3D 场景的当前朝向显示在场景中右上角的几何图形上

例如,如果想要创建向量来保存场景中原点的位置,可以使用如下代码:

Vector3 origin = new Vector(0f,0f,0f);

 上述代码将创建一个 Vector3 变量并使用0按顺序初始化位置值。foat 类型的值带不带小数点都可以,但是必须以小写的f结尾。
我们还可以使用 Vector2和 Vector3 类的属性来创建方向向量:

Vector3 forwardDirection = Vector3.forward;

 forwardDirection 变量指的是3D 空间中沿着z轴的场景方向,也就是前方,其中存储的并不是位置。本章后续部分会使用向量,现在只需要考虑 3D 运动的位置和朝向即可。

获取玩家输入

位置和朝向是很有用的概念,但只靠它们无法形成运动,还需要配合玩家输入才行。这就是引入 Input 类的原因所在,Input 类能将所有的按键输入和鼠标位置处理为相应的加速及陀螺仪数据。

对于 Hero Bomn 游戏来说,可使用键盘上的 W、A、S、D 键以及方向键来控制移动,并配合使用脚本,让相机能够跟随玩家光标所指的位置。为此,你需要理解输入轴是如何工作的。

使用 Edit|Project Settings|Input 打开InputManager 面板,

从中可以看到已配置好的 Unity 默认的输入列表,以 Horizontal 输入轴为例:Horizontal输入轴将Negative Button和 Positive Button 按钮分别设置为键位left和right;并且将AItNegative Button和Alt Positive Button 设置为键位a和d。

任何时候,当从代码中获取输入轴时,值的范围始终是 -1~1。例如,当左方向键或A 键被按下时,水平轴的值变为-1;释放按键时,值变回 0。同样,当右方向键或D键被按下时,水平输入轴的值会变化为 1。这种行为使你可以从代码中获取某个轴的不同输入,并且只需要一行代码,你不需要为了获取不同的值而输入一长串的if/else 语句。

 获取输入轴很简单,只需要调用 Input.GetAxis 方法并指定输入类型的名称即可,稍后我们将采用这种方式来获取水平和垂直输入。

既可以按自己的需要修改默认的输入配置,也可以通过递增 Size 的值来创建自定义输入轴,然后重命名创建出来的副本。

实践:移动玩家

为了使玩家可以移动,需要为 Player 对象添加脚本。

(1)在 Scripts 文件夹中创建一个新的 C#脚本,命名为 PlayerBehavior,然后将这个脚本拖动至Player对象上。

public class PlayerBehavior:MonoBehaviour
{
  public float moveSpeed=10f;
  public float rotateSpeed=75f;
  private float vInput;
  private float hInput;
  void Update()
  {
   vInput=Input.GetAxis("Vertical")*moveSpeed;
   hInput=Input.GetAxis("Horizontal")*rotateSpeed;
   this.transform.Translate(Vector3.forward*vInput*Time.Time*deltaTime);
   this.transform.Rotate(Vector3.up*hInput*Time.deltaTime);
  }
}

提示:
如果存在空的方法,例如本例中的 Start 方法,可以将其删除以保持代码清晰。当然,如果倾向于在脚本中保留它们,也可以。 

声明两个公共变量作为乘数:

  • moveSpeed表示玩家可以向前或向后移动的速度
  • rotateSpeed 表示玩家可以向左或向右旋转的速度

声明两个私有变量来保存来自玩家的输入,刚开始时不设置任何值:

  • vInput用来保存来自垂直轴的输入
  • hInput用来保存来自水平轴的输入

Input.GetAxis(Vertical")用于检查上下方向键以及 W和S键何时被按下,然后将其值乘以moveSpeed.

  • 上方向键和W键会返回 1,使得玩家向前方(正方向)移动。
  • 下方向键和S键会返回 -1,使得玩家向后方(反方向)移动。

Input.GetAxis("Horizontal")用于检查左右方向键以及A和D键何时被按下,然后将其值乘以rotateSpeed

  • 右方向键和D键会返回 1,使得玩家向右旋转。
  • 左方向键和A键会返回-1,使得玩家向左旋转

使用Translate方法移动Player 对象的Transform组件

  • 记住 this关键字指代当前脚本被附加到的游戏对象,在本例中也就是 Player对象。
  • 将 Vector3.forward 与Input 和 TimedeltaTime 相乘,后面两者提供了玩家沿着z轴向前或向后移动的速度和方向
  • 当游戏运行时,Time.deltaTime 会返回从上一到现在经历的时间,单位为秒.Time.deltaTime通常用于平滑 Update 方法获取的值,使其不受设备率的影响。

使用 Rotate 方法相对于传入的向量旋转玩家

  • ·将 Vector3.up 乘以hInput 和 Time.deltaTime 可得到向左或向右的旋转轴。
  • 这里使用this 关键字和TimedeltaTime 的原因与上面的相同。

如前所述,在 Translate和 Rotate 方法中使用方向向量只是达成目的的方式之也可根据轴输入创建新的 Vector3 变量并用作参数。

刚刚发生了什么
运行游戏后,就可以使用上/下方向键和 W/S键控制玩家前后移动,而使用左店方向键和A/D 键控制玩家左右旋转。我们仅仅使用很少的代码,就设置好了与帧率天关且易于修改的两个独立控件。然而,相机还不会跟随玩家移动,接下来就解决这问题

相机跟随

使一个对象跟随另一个对象的最简单方式就是将其中一个对象设置为另一个对象的子对象。但是,这意味着 Player 对象发生的任何移动或旋转都会影响到相机,这并不是我们想要的效果。幸运的是,Tansform类提供的方法使得相对于 Player 对象设相机的位置和旋转变得十分简单。

实践:编写相机行为

为了使相机的行为完全与 Player 对象的移动分离开来,我们需要控制相机相对于哪个目标来摆放,目标可以在Inspector 面板中进行设置。

(1)在Scripts 文件夹中创建一个新的C#脚本,命名为CameraBehavior,然后拖放至Main Camera对象上

public class CameraBehavior : MonoBehaviour
{
 public Vector3 camoffset = new Vector3(0,1.2,-2.6);
 private Transform target;
 void Start()
 {
  target=GameObject.Find("Player").transform;
  }
 void LateUpdate()
 {
  this.transform.position=target.TransformPoint(camOffset);
  this.transform.LOOkAt(target);
 }

下面对上述代码进行解释。

 声明一个Vector3变量来存储想要的 Main Camer 对象与Player 对象之间的偏郑距离

  • 可以在Inspoctor面板中手动设置相机的偏移位置,因为camOfiset 变量是公共的。
  • 现有的默认值比较合适,当然你也可以尝试修改

创建一个Transfom变量来保存 Player 对象的变换信息

  • 这使得我们可以获取位置、旋转和缩放信息
  • 这些信息不应该能够从 CameraBehavior 脚本之外进行访问,所以将 target变
    量设置为私有的

使用GameObject.Find 方法在场景中按名称查找Player 对象并获取Player对象的transfomm属性,这意味着存储在target 变量中的 Player对象的位置会在每一进行更新。

 LateUpdate是MonoBehaviour 脚本提供的方法,就像 Start和Update 方法一样,LateUpdate方法也在Update 方法之后执行。由于 PlayerBehavior 脚本在Update 方法中移动了Player对象,因此我们希望 CameraBehavior 脚本在移动操作完成之后执行;这祥可以确保 target变量引用的是最新位置。

每都把相机的位置设置为 target.TransformPoint(camOfset)以产生跟随效果

  • TransformPoint 方法用于计算并返回世界空间的相对位置。
  • 在这里,TransformPoint方法会返回 target 对象的偏移位置,x轴为0,y轴为1.2(将相机置于胶囊上方),z轴为 - 2.6(将相机置于胶囊后方),

LookAt方法会在每一更新胶囊的旋转值,使其朝向传入的Transform对象(本例中的 target 变量)所在的位置

同刚发生了什么

这里有大量知识需要消化吸收,如果按时间顺序进行分解的话,理解起来会简单一些。

(1)首先为相机创建了偏移位置

(2)然后查找并存储 Player 对象(也就是场景中的胶囊)的位置。

(3)最后,在每一帧都手动更新位置和朝向,使相机一直按设置好的距离跟随并朝向玩家。

使用Unity的物理系统

到目前为止,我们尚未讨论 Unity 引擎实际上是如何工作的以及如何在虚拟空间中创建出栩栩如生的交互与运动效果。本章的剩余部分将聚焦于 Unity 的物理系统

PhysX引擎中最重要的两个组件如下:

  • Rigidbody 组件,这种组件允许你使用重力以及其他因素对游戏对象施加影响
    Rigidbody 组件还会受施加的力的影响,从而产生更真实的运动效果Rigidbody组件的属性

  •  Collider 组件,这种组件决定了游戏对象何时以及怎样进入、离开其他对象的物理空间,抑或简单地碰撞并弹开。对于给定的游戏对象来说,只能添加个RigidBody组件,但可以添加多个 Collider 组件。Collider 组件的属性

 当两个游戏对象彼此碰撞时,Rigidbody 组件的属性决定了碰撞结果。举例来说如果一个游戏对象的质量比另一个大,那么轻一点的那个会被弹开,就像现实生活中一样。Rigidbody和 Collider 组件负责 Unity 中的所有物理交互。

在使用Rigidbody和Collider 组件时有一些注意事项,下面使用Unity 允许的移动类型的术语来进行解释.

  • Kincmatic移动发生在添加了 Rigidbody 组件的游戏对象上,但没有向场景t的物体系统进行注册。这种行为仅仅在某些特定情形下使用,可以通过选中Rigidbody组件的isKinematic 属性来启用。因为我们希望胶囊能够与物理系进行交互,所以这里不会使用这种运动方式。
  • Non-Kinematic移动指的是通过施加力来对 Rigidbody 组件进行移动和旋转而不是直接操作游戏对象的 transform 属性。我们的目标就是修改PlayerBehavior脚本以实现这种类型的移动。

提示:
我们现有的方式是操纵胶囊的 Transform 组件,同时使用 RigidBody 组件与物理系统进行交互,目的是想要在3D 空间中考虑移动与旋转。然而,这并不意味着可以在实际产品中使用,并且Unity 也建议避免在代码中混合使用Kinematic和Non-Kinematic 移动方式。

刚体运动

由于为Player对象添加了 Rigidbody 组件,因此我们应该使用物理引擎来控制移动而不是直接进行移动和旋转。力的施加方式有两种:

  • 直接使用 Rigidbody 类的AddForce 和AddTorque 方法分别移动或旋转对象这种方式存在一些不足,通常需要编写额外的代码以修正非预期的物理行为.
  • 使用其他的 Rigidbody 类方法,例如 MovePosition 和MoveRotation 方法。这种方式依然会施加力,但是系统会在幕后处理好边界情形。

实践:访问Rigidbody组件

我们首先需要从 Player 对象中获取并存储想要修改的 Rigidbody 组件。

(1)按如下代码修改 PlayerBehavior 脚本:

public class PlayerBehavior : MonoBehaviour
{
  public float moveSpeed = 10f;
  public float rotateSpeed = 75f;
  private float vInput;
  private float hInput;

  private Rigidbody _rb;

  void start()
  { 
   _rb = GetComponent<Rigidbody>();
  }
   
  void Update ()
  {
   vInput = InputGetAxis("Vertical") * moveSpeed;
   hInput = Input.GetAxis("Horizontal") * rotateSpeed;
   
   this.transform,Translate(Vector3.forward * vInput*Time.delatTime);
   
   this,transform.Rotate(Vector3.up * hInput * Time.deltaTime);
  }
}
 

 下面对上述代码进行解释。

添加一个私有的 Rigidbody 变量,用来存储胶囊的 Rigidbody 组件信息

Start 方法会在初始化脚本时触发,也就是单击 Play 按钮时。在初始化过程中设置变量时都应该使用Start 方法。

使用GetComponent方法检查脚本上附加的对象是否包含指定的组件类型,在本例中也就是 Rigidbody 组件。如果找到了,就返回。如果没有找到,那么返回 null.但在这里,我们已经知道Player对象上附有 Rigidbody 组件。

注释掉 Update 方法中对 Transform 和 Rotate 方法的调用,从而避免同时使用两种不同的控制方式。这里依然保留获取玩家输入的方式,以便后续继续使用。

实践:移动刚体

打开PiayerBehavior脚本,在Update 方法中添加如下代码并保存文件.

void FixedUpdate()
{
  Vector3 rotation = Vector3.up * hInput;
  Quaternion angleRot = Quaternion.Euler(rotation *Time.fixedDeltaTime);
  _rb.MovePosition(this.transform.position +this.transform.forward * vInput * Time.fixedDeltaTime);
 _rb.MoveRotation( rb.rotation * angleRot);
}

 下面对上述代码进行解释

任何物理的或 Rigidbody 相关的代码都要放在 FixedUpdate 方法中,而不是放在Update 或其他 MonoBehaviour 方法中

创建一个新的 Vector3 变量以存储左右旋转值。Vector3.up* hInput 与我们之前在Rotate 方法中使用的旋转向量是相同的。

Outernion.Eulcr 接收一个 Vector3 变量作为参数并使用欧拉角的格式返回旋转值。

  • 在MoveRotation 方法中,我们需要使用Qutemion 值而不是Vector3 变量,这是 Unity 首选的旋转类型的转换。
  • 这里乘以Timc.fixcdDeltaTime 的原因与在 Update 方法中乘以Time.deltaTime相同。

调用b组件的MovePosition 方法,该方法将接收一个 Vector3 变量作为参数并施加相应的力。

  • 使用的向量可以如下分解:胶囊的位置向量加上前向的方向向量与垂直输入和Time.fixedDeltaTime的乘积。
  • Rigidbody 组件负责调整施加的力以满足输入的向量参数

调用_b组件的MoveRotate 方法,该方法也将接收一个 Vector3 变量作为参数并施加相应的力。angleRot 已经包含来自键盘的水平输入,所以只需要将当前Rigidbody组件的旋转值乘以 angleRot 就能得到同样的左右旋转值。

刚刚发生了什么
如果现在运行游戏,就会发现玩家已经可以向着你看的方向前后移动了,同时还能沿着Y轴进行旋转。施加的力提供了相比直接平移/旋转更强大的效果,所以你需要调整好 Inspector 面板中的 moveSpeed 和 rotateSpeed 变量。至此,我们重建了之前已有的移动模式,并且拥有了更真实的物理效果。

碰撞体和碰撞

Collider 组件不仅仅使游戏对象能被 Unity 的物理系统认知到,也使交互和碰撞成为可能。可将碰撞体想象为围绕在游戏对象周围的不可见力场:取决于设置,它们可能被通过,也可能被撞上,并且有一系列方法会在发生不同的交互行为时触发。

Pickup_Prefab 对象层次中的Capsule 对象

 Capsule 对象周围的绿色形状是 Capsule Collider,可以通过 Center、Radius、Heig等属性进行移动和缩放。当创建基础图元时,碰撞体默认与图元形状匹配;因为现在创建了Capsule 图元,因此也会同时创建 Capule Collider。

碰撞体还支持Box、Sphere 和 Mesh 形状,可以手动从Compoent| Physics 菜单或单击Inpector 面板中的Add Component 按钮进行添加。

 举个例子,当两个带有碰撞体的游戏对象碰在一起时,它们都会发送OnCollisionEnter 消息,其中包含将要碰到的对象的引用。这种消息可用于各种交互式事件,比如拾取物品。

实践:拾取物品

 为了给 Pickup Item 更新碰撞逻辑,需要执行以下步骤。

(1)在Scripts 文件夹中创建一个新的C#脚本,命名为ItemBehavior,然后拖放至场景中Pickup Item 预制体之下的 Capsule 对象上,如图7-9 所示。注意,任何使用了碰撞检测的脚本都必须被附加到包含了 Collider 组件的游戏对象上,即使是预制体的子对象。
(2)使用Pickup Item 更新根预制体

(3)在ItemBehavior脚本中添加如下代码并保存:

public class ItemBehavior : MonoBehaviour
{
  void OnCollisionEnter(Collision collision)
  {
    if(collision.gameObject.name == “Player")
    {
     Destroy(this.transform.parent.gameObject);
     Debug.Log("Item collected!");
    }
   }

}

 (4)单击 Play 按钮,移动玩家至胶囊处并捡起胶囊!

下面对步骤(3)中的代码进行解释。

1.当把另一个对象移至 Pikcup_Item 且 isTrigger 处于关态时,Umy 会自动调用OnCollisionEnter 方法。

  • OnCollisionEnter 方法有一个参数用于存储 Collider 引用。
  • 注意 collision 变量的类型是 Collision 而不是 Collider。

2.Collision 类的 gamebject 属性用于保存对 GameObject 碰撞体的引用。
以使用gameObject属性获取游戏对象的名称并使用if语检查碰撞体是否是Play对象。

3.如果碰撞体是 Player 对象,就调用 Destroy 方法,该方法接收一个游戏对象作为参数。

  • 我们必须使整个Pickup Item对象被销毁,而不仅仅是销毁 Capsule 对象
  • 因为 ItemBehaivor 脚本被附加到了 Capsule 对象上,而 Capsule 对象又是Pickup_Item 对象的子对象,所以可以使用 this.transform.parentgameObject将Pickup Item 对象销毁
  • 向控制台打印一条日志,指明已经收集了道具。

刚刚发生了什么

我们在本质上相当于将 lemBehavior 脚本设置为监听与 Pikup_Item 预制体Capsule 子对象发生的任何碰撞。每当发生碰撞时,ItemBehavior 脚本就会使OnCollisionEnler 方法检查碰撞对象是否为 Player 对象。如果是,就销毁(或收集)。果感到困惑,请将编写的碰撞相关代码当作来自 Pickup Item 预制体的通知的接收者每当胶囊被碰撞时,就会触发这些代码。

提示:
也可以创建类似的包含OnCollisionEnter方法的脚本并附加到Player对象上然后检测是否与 Pickup Item 预制体发生了碰撞。碰撞逻辑取决于碰撞对象的角度

使用碰撞体触发器

默认情况下,碰撞体的 isTrigger 属性并未启用,物理系统会把这些碰撞体视为实体。然而,某些情况下我们需要使游戏对象可以穿过碰撞体,触发器就是为了处理这种情况而存在的。当isTrigger 属性被启用后,游戏对象就可以穿过碰撞体,但发送的通知会变为OnTriggerEnter、OnTriggerExit 和OnTriggerStay。

触发器多用于检测游戏对象是否进入某个特定区域或通过某个点。可使用触发器在敌人周围设置警戒区域,如果玩家进入触发区域,敌人就会受到惊扰,然后开始攻击玩家。

实践:创建敌人

为了创建敌人,需要执行以下步骤:

(1)在Hierarchy 面板中使用 Create 3D Object|Capsule 创建一个新的 Capsule图元,命名为 Enemy。

(2)在Materials 文件夹中使用Create|Material创建一个材质,命名为 Enemy_Mat,设置AIbedo属性为亮红色。然后拖动Enemy Mat 材质至 Enemy游戏对象上。

(3)选中Enemy游戏对象,单击Add Component按并搜索 Sphere Collider,然后按Enter键进行添加。选中is Trigger 复选框并将Radius设置为8.

 刚刚发生了什么
新建的Enemy游戏对象现在围绕着一个半径为8的球形触发器。任何时候,当另一个对象进入、停留或离开时,Unity 都会发送能够被捕获到的通知,就像处理碰撞时一样。

实践:捕获触发器事件

为了捕获触发器事件,需要执行如下步骤

(1)在Scripts文件夹中创建一个新的C#脚本,命名为 EnemyBehavior,然后拖至Enemy游戏对象上。

(2)添加如下代码并保存文件。

public class EnemyBehavior : MonoBehaviour
{
 void OnTriggerEnter(Collider other)
 {
  if(other.name=="Player")
  {
   Debug.Log('Player detected - attack!");
  }
 }
 
void OnTriggerExit(Collider other)
{
 
 if(other.name=="Player")
 {
  Debug.Log("Player out of range,resume patrol");
 }
}
}

(3)单击 Play 按钮,走向Enemy 游戏对象以触发第一个通知,然后远离 Enemy游戏对象以触发第二个通知。

下面对步骤(2)中的代码进行解释。

任何时候,当一个对象进入 Enemy 游戏对象的球形触发器时,OnTriggerEnter方法就会被触发。

  • 类似于 OnCollisionEnter 方法,OnTriggerEnter 方法的参数用于存储对象的Collider组件的引用。
  • 注意参数对象的类型是 Collider 而不是 Collision。

使用other 获取碰撞体对象的名称并使用if语句检查是不是 Player 对象。如果是,就输出 Player 对象位于危险区域的提示信息

当对象离开Enemy游戏对象的球形触发器时,触发OnTriggerExit 方法。

使用f 语句按名称检查离开球形触发器的对象。如果是 Player 对象,就将另一条信息打印到控制台,指示玩家现在是安全的

 刚刚发生了什么

 Enemy游戏对象的球形触发器会在自身被侵入时发送通知,EnemyBehavior脚本将捕获这些事件。任何时候,当玩家进入或离开球形触发器时,都会在控制台中打印调试日志以确保代码正常工作。

总结

  • Rigidbody组件能为附加到的对象添加真实的物理模拟。
  • Collider 组件之间可以相互交互,并且 Collider 组可以作为对象与 Rigidbo组件进行交互。
  • 如果一个对象使用了 Rigidbody 组件但没有启用 isKinematic 属性,那么得的就是运动学效果,因为物理系统会忽略这个对象。
  • 如果一个对象使用了 Rigidbody 组件并且施加了力和扭矩,那么得到的将是非运动学效果。
  • 碰撞体基于交瓦行为发送通知。

在本章,你创建了自己的第一款游戏,并积累了一定的经验。现在,你已经能使用向量和基本的向量运算来确定 3D 空间中的位置和角度,并且熟悉了玩家输入及移动和旋转 GameObject 的两种主要方法。你甚至深入接触了 Unity 的物理系统,熟悉了刚体、碰撞、触发器以及事件通知等知识。总而言之,你为 Hero Bor 游戏开了个好头。

下面将开始处理更多的游戏机制,包括跳跃、冲刺、射击以及与环境中的各个部分进行交互。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/22364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第3章 数据结构中树的概念

文章目录文档配套视频讲解链接地址第03章 树3.1 二叉树创建1. 实例20 二叉树创建3.2 二叉树遍历1. 二叉树的遍历2. 三种遍历算法3. 实例21 二叉树的遍历文档配套视频讲解链接地址 腾讯课堂视频链接地址 : 24_树与二叉树_树的理解腾讯课堂视频链接地址 : 25_树与二叉树_二叉树遍…

论文精读:End-to-End Semi-Supervised Object Detection with Soft Teacher

https://arxiv.org/pdf/2106.09018.pdf 半监督学习重在流程&#xff0c;而不在网络细节 Abstract 本文提出了一种端到端半监督目标检测方法&#xff0c;不同于以往更复杂的多阶段方法。端到端训练逐渐提高了训练过程的伪标签质量&#xff0c;越来越准确的伪标签进而有利于目…

法大大携手企企科技,助力企业实现全生命周期合同管理

伴随着企业信息化高速发展&#xff0c;打破信息孤岛实现全业务信息化的需求日益凸显&#xff0c;企企科技携手法大大&#xff0c;完善了客户从签署到项目管理全场景的信息化解决方案&#xff0c;全面助力客户实现项目管控信息化。“数字化技术对项目管理具有多角度、全方位的影…

6-JS的Fetch 跨域问题

跨域访问 只要协议、主机、端口之一不同&#xff0c;就不同源&#xff0c;例如 http://localhost:7070/a 和 https://localhost:7070/b 就不同源 同源检查是浏览器的行为&#xff0c;而且只针对 fetch、xhr 请求 如果是其它客户端&#xff0c;例如 java http client&#xff…

oracle开启归档日志并修改归档日志路径

一、归档日志概念 归档日志&#xff1a;非活动的重做日志&#xff0c;用来保存所有的重做历史记录。 什么时候写归档日志&#xff1a;当数据库处于archivelog模式并重做日志切换的时候&#xff0c;后台进程ARCH会启动将重做日志的内容完整的保存到归档日志中。 日志操作模式…

ZPM介绍(2)

发布您自己的软件 首先&#xff1a;要发布您的软件&#xff0c;您要支持这个”命名规范。其中和zmp最相关的是包名和l类名的设计&#xff0c;你要定义成这样&#xff1a; company.project.subpackage.TheClass.cls 如果您的Package Name定义是&#xff1a; Company.Project, 有…

元数据管理-解决方案调研二:元数据管理解决方案——Saas/内部解决方案(3)

Saas/内部解决方案 2.10、Netflix Metacat Metacat 是一种元数据服务&#xff0c;使数据易于发现、处理和管理。在 Netflix&#xff0c;数据仓库由存储在 Amazon S3&#xff08;通过 Hive&#xff09;、Druid、Elasticsearch、Redshift、Snowflake 和 MySql 中的大量数据集组…

【Linux入门指北】文件服务器

文件服务器 文章目录文件服务器一、FTP Server1.简介2.FTP Server 默认配置3.FTP Clinet4.vsftpd的主动和被动模式二、NFS Server1.简要介绍2.环境配置3.关闭防火墙4.nfs(存储端)5.web1 web2 web3 客户端5.1 安装NFS客户端5.2 开启httpd服务5.3 查看存储端共享5.4 手动挂载5.5 …

电容笔哪个牌子好?2022年电容笔十大品牌排行榜

当电容笔搭配上了ipad&#xff0c;可以大大提升我们的工作效率&#xff0c;不会变得乏味。对于那些对绘画要求很高的人来说&#xff0c;电容笔在绘画中的作用更是不容忽视的。其实我个人对电容笔这块了解还不少的&#xff0c;有着许多平替电容笔都支持在ipad上使用的&#xff0…

社会工程攻击依然是企业面临的最大威胁

企业进入数字化时代&#xff0c;网络攻击行为无处不在&#xff0c;利用社会工程攻击已成黑客的惯用手段。研究表明&#xff0c;91%的网络攻击是通过社会工程手段完成的。 常见的社会工程攻击手段有哪些&#xff1f; 网络钓鱼&#xff1a; 这是经典手段&#xff0c;大多数的钓…

HTML5期末大作业:基于html企业官网项目的设计与实现【艺术官网】

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Alien Skin Exposure2023调色滤镜插件RAW后期处理软件

Exposure既可以作为ps、lr的插件使用&#xff0c;也可以单独作为一款专业的图像编辑器使用&#xff0c;它可以处理RAW格式的照片&#xff0c;拥有500多种预设滤镜&#xff0c;还有照片管理和添加文字水印等功能。可以说&#xff0c;无论是新手还是设计师和摄影师&#xff0c;都…

【POJ No. 2431】 丛林探险 Expedition

【POJ No. 2431】 丛林探险 Expedition 北大OJ 题目地址 【题意】 一群人开着一辆卡车冒险进入丛林深处&#xff0c;卡车油箱坏了&#xff0c;每走1米就会漏1升油&#xff0c;他们需要到最近的城镇&#xff08;距离不超过106米&#xff09;修理卡车。卡车当前位置和城镇之间有…

怎么将视频转化为gif?

如何将视频转化为gif&#xff1f;gif是一种大家平时常见的动态图片格式&#xff0c;动图是一种非常有意思的图片种类&#xff0c;gif一般都是一些非常有意思的小动图&#xff0c;例如我们在群聊时使用的动态表情包&#xff0c;还有一些球迷朋友喜欢看的足球进球动图等。gif动图…

asp核酸检测预登记系统源码

用asp开发的核酸检测预登记系统上线了&#xff0c;用户填写姓名&#xff0c;手机&#xff0c;身份证号&#xff0c;地址等信息后生成一个加密的二维码&#xff0c;管理员扫码后可以得到真实的二维码文字信息。主要为方便核酸采集统计托底等&#xff0c;也可以用作会议入场信息采…

【NLP】使用 PyTorch 通过 Hugging Face 使用 BERT 和 Transformers 进行情感分析

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

初识Kafka构造组成

在刚学习kafka的时候&#xff0c;有时候会比较纳闷broker是什么&#xff1f;topic又是什么&#xff1f;一台机器上有多少broker&#xff1f;又有多少的topic和partition&#xff1f;由下面这张图来初识我们的kafka&#xff1a; 上图中包含了一个kafka集群的所有组件&#xff1a…

大数据毕业设计题目推荐 毕设选题大全

文章目录0 前言1 如何选题1.1 选题技巧&#xff1a;如何避坑(重中之重)1.2 为什么这么说呢&#xff1f;1.3 难度把控1.4 题目名称1.5 最后2 大数据 - 选题推荐2.1 大数据挖掘类2.2 大数据处理、云计算、区块链 毕设选题2.3 大数据安全类2.4 python大数据 游戏设计、动画设计类2…

LeetCode-808. 分汤【动态规划,概论与统计,记忆化搜索】

LeetCode-808. 分汤【动态规划&#xff0c;概论与统计&#xff0c;记忆化搜索】 题目描述&#xff1a;解题思路一&#xff1a;动态规划&#xff0c;这里将所有的汤除了25&#xff0c;缩小数值。自底向上解题思路二&#xff1a;记忆化搜索&#xff0c;自顶向下搜索&#xff0c;会…

R summarize()分组摘要

summarize()分组摘要分组平均值最大最小值计数 library(nycflights13) library(tidyverse)summarize()可以将数据折叠成一行 如果不与group_by()一起使用&#xff0c;那么summarize()也没什么用 summarize(flights, delay mean(dep_delay, na.rm TRUE))delay12.63907 group…