猫钩增加了游戏的趣味性和趣味性。 您可以使用它们来移动关卡,在竞技场中战斗并获得物品。 但是,尽管表面上看起来很简单,但是绳索管理的物理原理和现实行为的创建仍具有挑战性!
在本教程的第一部分中,我们实现了自己的二维“挂钩”系统,并学习以下内容:
- 创建一个瞄准系统。
- 使用线渲染器和距离关节创建绳索。
- 我们将教绳子缠绕游戏对象。
- 计算绳索的摆动角度并增加该方向的强度。
注意 :本教程面向高级和有经验的用户,不涉及诸如添加组件,创建新的GameObject脚本和C#语法之类的主题。 如果您需要提高Unity技能,请查看我们的Unity入门和Unity脚本简介教程。 由于本教程中使用了DistanceJoint2D ,因此您还应该浏览Unity 2D中的Physics Joints ,然后才返回本教程。
开始工作
下载本教程的
草稿 ,然后在Unity编辑器中将其打开。 操作需要Unity 2017.1或更高版本。
从“
场景”文件夹中打开“
游戏”场景,然后查看我们将从何处开始:
现在,我们有一个简单的玩家角色(子弹)和石头悬在空中。
到目前为止,GameObject
Player的重要组件是胶囊碰撞器和刚体,它们可以使它与该级别的物理对象进行交互。 另外,将简单的动作脚本(
PlayerMovement )附加到角色上,使其可以在地面上滑行并执行简单的跳跃。
按“
播放”按钮开始游戏,然后尝试控制角色。
A和
D左右移动它,当您按下
空格键时,它会跳转。 尽量不要滑倒并掉下悬崖,否则会死!
我们已经有了管理的基础知识,但是现在最大的问题是缺少挂钩。
制作钩子和绳索
乍一看,“猫钩”系统看起来很简单,但是对于其高质量实现而言,必须考虑许多方面。 以下是二维猫钩机制的一些要求:
- 线渲染器显示绳索。 当绳索缠绕对象时,我们可以向线渲染器添加更多的线段,并将顶点放置在与绳索的断点相对应的点上。
- DistanceJoint2D。 它可以用于连接猫钩的当前锚点,以便我们的子弹可以摆动。 它还允许我们调整可用于延长和减少绳索的距离。
- 带有RigidBody2D的子GameObject,可以根据挂钩的锚点的当前位置进行移动。 本质上,它将是绳索的悬挂点/锚点。
- Raycast用于抛出钩子并附加到对象上。
在“层次结构”中选择
Player对象,然后向其添加一个名为
RopeHingeAnchor的新子
GameObject 。 此GameObject将用于定位猫钩的悬挂点/锚点。
将
SpriteRenderer和
RigidBody2D组件添加到
RopeHingeAnchor中 。
对于SpriteRenderer,将
Sprite属性设置为使用
UISprite值,并将
Layer中的Order更改为
2 。 通过
取消选中组件名称旁边
的框来禁用该组件。
对于
RigidBody2D组件
,将“身体类型”属性设置为
Kinematic 。 这一点将不通过物理引擎移动,而是通过代码移动。
选择“
绳索”层,并将“变换”组件的X和Y缩放比例值设置为
4 。
再次选择
Player并附加新的
DistanceJoint2D组件。
将
RopeHingeAnchor从层次结构拖动到
DistanceJoint2D组件的
Connected Rigid Body属性上,然后关闭“
自动配置距离” 。
在
Scripts项目文件夹中创建一个名为
RopeSystem的新C#脚本,然后在代码编辑器中将其打开。
删除
Update
方法。
在
RopeSystem
类
RopeSystem
内的脚本顶部
RopeSystem
添加新变量,
Awake()
方法和新
Update
方法:
让我们按顺序分析每个部分:
- 我们使用这些变量来跟踪RopeSystem脚本将与之交互的各种组件。
Awake
方法从游戏开始时开始,并禁用ropeJoint
(DistanceJoint2D组件)。 它还将playerPosition
设置为Player的当前位置。- 这是主
Update()
循环中最重要的部分。 首先,我们使用ScreenToWorldPoint
摄像头ScreenToWorldPoint
获得鼠标光标在世界上的位置。 然后,我们通过从鼠标在世界上的位置减去玩家的位置来计算凝视的方向。 然后,我们用它来创建aimAngle
,它表示光标的瞄准角度。 该值在if构造中存储一个正值。 aimDirection
是一种在以后派上用场的方法。 我们仅对Z值感兴趣,因为我们正在使用2D摄像机,并且这是唯一对应的OS。 我们传递aimAngle * Mathf.Rad2Deg
,它将弧度角转换为角度(以度为单位)。- 使用方便的变量跟踪播放器的位置,该变量使您不必经常引用
transform.Position
。 - 最后,我们有了
if..else
构造,我们将很快使用它来确定绳索是否连接到锚点。
保存脚本并返回到编辑器。
将
RopeSystem组件附加到Player对象,并将各种组件挂在我们在
RopeSystem脚本中创建的公共字段上。 将
Player ,
Crosshair和
RopeHingeAnchor拖动到适当的字段中:
- 绳铰链锚 :RopeHingeAnchor
- 绳接头 :球员
- 十字线 :十字线
- 十字准线精灵 :十字准线
- 球员运动 :球员
现在,我们只是在进行所有这些复杂的计算,但是到目前为止,还没有可视化可以显示它们的实际效果。 但请放心,我们会尽快进行。
打开
RopeSystem脚本并向其中添加新方法:
private void SetCrosshairPosition(float aimAngle) { if (!crosshairSprite.enabled) { crosshairSprite.enabled = true; } var x = transform.position.x + 1f * Mathf.Cos(aimAngle); var y = transform.position.y + 1f * Mathf.Sin(aimAngle); var crossHairPosition = new Vector3(x, y, 0); crosshair.transform.position = crossHairPosition; }
此方法根据传输的
aimAngle
(我们在
Update()
计算的浮点值)定位瞄准器,以使其围绕播放器旋转,半径为1个单位。 如果还没有,还包括一个sprite作用域。
在Update()
我们将!ropeAttached
构造更改为检查!ropeAttached
,使其看起来像这样:
if (!ropeAttached) { SetCrosshairPosition(aimAngle); } else { crosshairSprite.enabled = false; }
保存脚本并运行游戏。 现在,我们的弹应该能够瞄准。
下一个需要实现的逻辑是猫钩。 我们已经确定了瞄准的方向,因此我们需要一种将其作为参数接收的方法。
在
RopeSystem脚本中的变量下方添加以下变量:
public LineRenderer ropeRenderer; public LayerMask ropeLayerMask; private float ropeMaxCastDistance = 20f; private List<Vector2> ropePositions = new List<Vector2>();
LineRenderer将包含指向绘制绳索的线渲染器的链接。
LayerMask允许您自定义挂钩可以与之交互的物理层。
ropeMaxCastDistance
值设置光线投射可以“拍摄”的最大距离。
最后,Vector2的位置列表将用于跟踪绳索的包裹点,我们将在后面进行讨论。
添加以下新方法:
这是上面的代码的作用:
- 从
Update()
循环调用HandleInput,并简单地轮询鼠标左键和右键的输入。 - 当单击左键时,绳线将打开,并从玩家的位置朝瞄准方向发射2D射线广播。 设置最大距离,以使钩猫无法在无限距离处拍摄,并应用遮罩,以便可以选择射线投射可以碰撞的物理层。
- 如果检测到
ropeAttached
投射,则ropeAttached
为true
,并检查绳索顶点位置的列表,以确保那里没有点。 - 如果检查结果为true,则将一小股冲力加到弹头上,使其弹跳到地面之上,
ropeJoint
(DistanceJoint2D)设为打开,该距离设置为等于弹头和射线投射命中点之间的距离。 还包括一个锚点精灵。 - 如果raycast没有击中任何东西,则禁用线渲染器和ropeJoint,并且
ropeAttached
标志为false。 - 如果按下鼠标右键,则会
ResetRope()
方法,该方法将禁用所有与绳索/挂钩相关的参数并将其重置为如果不使用挂钩则应具有的值。
在我们的
Update
方法的最底部,添加对新
HandleInput()
方法的调用,然后将
aimDirection
值传递给
aimDirection
:
HandleInput(aimDirection);
将更改保存到
RopeSystem.cs并返回到编辑器。
加一根绳子
我们的鼻涕虫如果没有绳索就无法在空中飞翔,因此现在该给它做些东西了,这将是绳索的视觉表示,并且具有“转弯”的能力。
线渲染器是理想的选择,因为它允许我们转移点的数量及其在世界空间中的位置。
这里的想法是,我们总是将绳索的第一个顶点(0)存储在玩家的位置,并且当绳索应该缠绕某些东西时,所有其他顶点都将动态定位,包括铰链的当前位置,这是绳索的下一个点来自玩家。
选择
播放器,然后向其中添加
LineRenderer组件。 将
宽度设置为
0.075 。 展开“
材料”列表,并选择位于项目“
材料”文件夹中的“
RopeMaterial”材料作为“
元素0” 。 最后,对于Line Renderer,对于
Texture Mode,选择“
Distribute Per Segment” 。
将“线渲染器”组件拖动到“
绳索系统”组件的“
绳索渲染器”字段中。
单击“
绳索层蒙版” (
Rope Layer Mask)的下拉列表,然后选择作为raycast
默认,“绳索”和“透视”可以与之交互的层。 因此,“射击”射线广播时,它将仅与这些图层碰撞,而不会与其他对象(例如播放器)碰撞。
如果您现在开始游戏,您会发现奇怪的行为。 当我们瞄准子弹头上方的一块石头并用钩子射击时,我们会向上跳一小段,然后我们的朋友开始表现得相当随意。
我们尚未设置距离关节的距离,此外,还没有配置线渲染的顶点。 因此,我们看不到绳索,并且由于距离关节位于子弹位置的正上方,因此距离关节距离的当前值会将其向下推到其下方的石头上。
但是不用担心,现在我们将解决此问题。
在
RopeSystem.cs脚本中
,在类的开头添加一个新的运算符:
using System.Linq;
这使我们能够使用LINQ查询,在我们的情况下,这仅使我们能够方便地找到
ropePositions
列表的第一个或最后一个元素。
注意 :语言集成查询(LINQ)是基于直接在C#中嵌入查询功能的一组技术的名称。 您可以在此处了解更多信息。
在其他变量下添加一个新的bool私有变量,命名为
distanceSet
:
private bool distanceSet;
我们将使用此变量作为标志,以便脚本可以识别出绳索的距离(对于演奏者和当前连接猫钩的参考点之间的点)的设置正确。
现在添加一个新方法,我们将使用它来设置渲染线的绳索顶点的位置,并在已存储的绳索位置列表中设置距离关节的距离(
ropePositions
):
private void UpdateRopePositions() {
解释上面显示的代码:
- 如果未连接绳索,请退出该方法。
- 我们将绳线渲染点的值分配给
ropePositions
存储的位置数,再加上1(代表玩家的位置)。 - 我们在
ropePositions
列表周围ropePositions
,并为每个位置(最后一个除外) ropePositions
,将线渲染器顶点的位置分配给ropePositions
列表中循环索引存储的Vector2位置的值。 - 从绳索的最终位置开始,将第二个位置分配给绳索的锚点,当前铰链/锚点应位于该位置,或者如果我们只有绳索的一个位置,则将其设为锚点。 因此,我们将
ropeJoint
的距离设置为等于播放器与绳索当前位置之间的距离,我们ropeJoint
在循环中ropeJoint
。 - if构造处理绳索在环中的当前位置排在末端第二个位置的情况; 即绳索连接到物体的点,即 当前的铰链/锚点。
- 此
else
块处理绳索的最后一个顶点的位置到玩家当前位置值的分配。
请记住在
Update()
UpdateRopePositions()
的末尾添加
UpdateRopePositions()
调用:
UpdateRopePositions();
将更改保存到脚本,然后再次运行游戏。 用钩子对准角色上方的一块石头并射击,从而使“小空间”跳得很小。 现在您可以享受劳动成果了-子弹在石头上平静地摇摆。
现在您可以转到场景窗口,选择``播放器'',使用移动工具(默认情况下为
W键)将其移动,并观察绳索线的两个顶点渲染如何跟随钩子的位置以及播放器的位置来绘制绳索。 释放播放器后,DistanceJoint2D会正确计算距离,并且子弹将继续在连接的铰链上摆动。
处理包装点
到目前为止,与挥动的子弹一起玩并不比防水毛巾有用,因此我们绝对需要对其进行补充。
好消息是,将来可以使用新添加的处理绳索位置的方法。 到目前为止,我们仅使用两个绳索位置。 一个连接到玩家的位置,第二个连接到钩子在射击时的锚点的当前位置。
唯一的问题是,尽管我们没有跟踪绳索的所有潜在位置,但这需要做一些工作。
为了识别绳索应该缠绕在石头上的位置,并向线条渲染器添加新的顶点位置,我们需要一个系统来确定对撞机顶点是否位于子弹当前位置和绳索当前铰链/锚点之间的直线之间。
看起来这是对旧的raycast的重新工作!
首先,我们需要创建一种方法,该方法可以根据射线投射的碰撞点和对撞机的边缘找到对撞机中最接近的点。
将新方法添加到
RopeSystem.cs脚本中:
如果您不熟悉LINQ查询,那么此代码可能看起来像某种复杂的C#魔术。
如果是这样,那就不要害怕。 LINQ为我们做了很多工作:
- 此方法采用两个参数-RaycastHit2D对象和PolygonCollider2D对象 。 关卡上的所有石头都具有PolygonCollider2D碰撞器,因此,如果我们始终使用PolygonCollider2D形状,它将可以正常工作。
- 这就是LINQ查询魔术开始的地方! 在这里,我们将多边形对撞机的点集合转换为Vector2位置字典 ( 字典中每个元素的值就是位置本身),并且为每个元素的关键点分配了从该点到玩家位置玩家的距离(浮动值)。 有时在这里发生其他事情:将生成的位置转换为世界空间(默认情况下,对撞机顶点的位置存储在本地空间中,即相对于对撞机所属对象的本地位置,我们需要在世界空间中定位)。
- 字典按键排序。 换句话说,最接近玩家当前位置的距离。 返回最接近的距离,也就是说,此方法返回的任何点都是播放器与绳索铰链当前点之间的对撞点!
让我们回到
RopeSystem.cs脚本并在顶部添加一个新的私有字段变量:
private Dictionary<Vector2, int> wrapPointsLookup = new Dictionary<Vector2, int>();
我们将使用它来跟踪绳索可以缠绕的位置。
在
Update()
方法的末尾,找到包含
crosshairSprite.enabled = false;
的
else
结构
crosshairSprite.enabled = false;
并添加以下内容:
解释这段代码:
- 如果某些位置存储在
ropePositions
列表中,则... - 我们从玩家的位置朝着从列表中查看绳索的最后位置的方向射击(即猫钩在石头上的固定点),光线投射距离等于玩家与绳索的固定点位置之间的距离。
- 如果光线投射与某些物体碰撞,则此对象的碰撞器将安全地转换为PolygonCollider2D类型。 虽然它是真正的PolygonCollider2D,但使用我们之前写为Vector2的方法返回了此碰撞器的最接近顶点位置。
wrapPointsLookup
对其进行检查,以确保不会再次检查相同位置。 如果已选中,则我们丢弃绳索并将其剪断,从而使播放器掉落。- 然后,
ropePositions
列表:添加了绳索应该缠绕的位置。 wrapPointsLookup
字典也已更新。 最后,将distanceSet
标志重置,以便UpdateRopePositions()
方法可以使用新的绳索长度和线段重新定义绳索的距离。
在
ResetRope()
添加以下行,以便每次玩家断开绳索连接时
wrapPointsLookup
清除
wrapPointsLookup
字典:
wrapPointsLookup.Clear();
保存并启动游戏。 将猫钩子射入子弹上方的石头中,然后使用“场景”窗口中的“移动”工具将子弹移过几个石头壁架。
这就是我们教绳子缠绕物体的方式!
增加摇摆能力
挂在绳子上的弹头非常静态。 为了解决这个问题,我们可以增加摆动能力。
为此,我们需要获得一个垂直于向前摆动(侧向)的位置的位置,而不管他所看的角度如何。
打开
PlayerMovement.cs并将以下两个公共变量添加到脚本顶部:
public Vector2 ropeHook; public float swingForce = 4f;
ropeHook
变量分配绳索钩子当前所在的任何位置,并且
swingForce
是我们用来添加挥杆动作的值。
用新的方法替换
FixedUpdate()
方法:
void FixedUpdate() { if (horizontalInput < 0f || horizontalInput > 0f) { animator.SetFloat("Speed", Mathf.Abs(horizontalInput)); playerSprite.flipX = horizontalInput < 0f; if (isSwinging) { animator.SetBool("IsSwinging", true);
这里的主要变化是首先检查该标志,isSwinging
以便仅在将子弹悬垂在绳索上时才执行操作,并且我们还添加一个垂直于子弹角的垂直线,以指示其当前的锚点在绳索顶部,但垂直于其摆动方向。- 我们得到从玩家到弯钩连接点的归一化方向向量。
- 根据子弹向左或向右摇摆,使用计算垂直方向
playerToHookDirection
。还添加了一个调试绘制调用,以便您可以根据需要在编辑器中看到它。
打开RopeSystem.cs并将以下内容添加到方法内部else块的顶部:if(!ropeAttached)
Update()
playerMovement.isSwinging = true; playerMovement.ropeHook = ropePositions.Last();
在相同设计的if块中,if(!ropeAttached)
添加以下内容: playerMovement.isSwinging = false;
因此,我们告知PlayerMovement脚本玩家正在摆动,并确定绳索的最后一个位置(除了玩家的位置),换句话说,就是绳索的锚点。这是计算我们刚刚添加到PlayerMovement脚本中的垂直角度所必需的。如果在正在运行的游戏中打开小控件并按A或D向左/向右摆动,则外观如下所示:增加绳索下降
虽然我们没有能力上下移动绳索。尽管在现实生活中,不会轻易随绳索上升或下降,但这是一场万事皆有的游戏,对吧?在RopeSystem脚本的顶部,添加两个新的字段变量: public float climbSpeed = 3f; private bool isColliding;
climbSpeed
会设定弹头在绳索上上下移动的速度,并将isColliding
用作标记来确定是否可以增加或减小远距接头绳索的远距接头特性。添加此新方法: private void HandleRopeLength() {
该块if..elseif
沿垂直轴(键盘上的上/下或W / S)读取输入,并考虑到标记,ropeAttached iscColliding
增加或减小距离ropeJoint
,从而产生延长或缩短绳索的效果。我们钩住此方法,将其调用添加到末尾Update()
: HandleRopeLength();
我们还需要一种设置标志的方法isColliding
。在脚本底部添加以下两个方法: void OnTriggerStay2D(Collider2D colliderStay) { isColliding = true; } private void OnTriggerExit2D(Collider2D colliderOnExit) { isColliding = false; }
这两个方法是MonoBehaviour脚本基类的本机方法。如果Collider当前触摸游戏中的另一个物理对象,则该方法将不断触发OnTriggerStay2D
,并为flag分配一个isColliding
值true
。这意味着,当弹头碰到石头时,isColliding标志被分配一个值true
。当一个对撞机离开另一对撞机区域时将OnTriggerExit2D
触发该方法,并将标志设置为false。请记住:该方法OnTriggerStay2D
在计算上可能非常昂贵,因此请谨慎使用。接下来要去哪里?
重新开始游戏,这次按箭头键或W / S上下移动绳索。本部分教程的完成项目可以在此处下载。我们已经走了很长一段路-从无摇摆的sl到杂技无壳腹足纲软体动物!您已经了解了如何创建一种瞄准系统,该系统可以在具有对撞机的任何物体上射出一个猫钩,紧紧抓住并同时摆动,在物体边缘周围绕动一根动态绳!辛苦了但是,这里缺少重要功能-绳索在必要时不能“解开”。在本教程的第二部分中,我们将解决此问题。但是,如果您愿意冒险,那么为什么不尝试自己做呢?您可以为此使用字典wrapPointsLookup
。