在Unity中创建猫钩。 第二部分

图片

注意 :本教程面向高级和有经验的用户,不涉及诸如添加组件,创建新的GameObject脚本和C#语法之类的主题。 如果您需要提高Unity技能,请查看我们的Unity入门Unity脚本简介教程。

在本教程的第一部分中 ,我们学习了如何使用将绳索缠绕在障碍物上的机制来创建猫钩。 但是,我们还需要更多:绳索可以在水平面上缠绕对象,但返回时不会分离。

开始工作


从Unity的第一部分中打开完成的项目,或下载本部分教程的草稿 ,然后打开2DGrapplingHook-Part2-Starter 。 与第一部分一样,我们将使用Unity版本2017.1或更高版本。

在“ 场景”项目文件夹中的编辑器中打开“ 游戏”场景。


启动游戏场景,尝试将猫钩钩在角色上方的石头上,然后摆动以将绳子缠绕在一对石头边缘上。

当您返回时,您会注意到绳索用来转过的石头的点不会再次脱钩。


考虑绳索应展开的位置。 为了简化任务,最好使用绳索缠绕边缘的情况。

如果嵌在其头部上方的石头上的子弹向右摆动,则绳索将在阈值之后弯曲,在该阈值处,该子弹与当前子弹所连接的肋骨交叉180度角点。 在下图中,它以突出显示的绿点显示。


当子弹向另一个方向摆动时,绳索应再次在同一点解钩(上图中以红色突出显示):


松开的逻辑


要计算需要在绳索较早缠绕的点上解开绳索的力矩,我们需要了解几何形状。 特别是,我们将使用角度的比较来确定绳索何时应从边缘分离。

这项任务似乎有些令人生畏。 即使在最勇敢的时候,数学也会激发恐惧和绝望。

幸运的是,Unity具有一些出色的数学助手功能,可以使我们的生活更加轻松。

在IDE中打开RopeSystem脚本,并创建一个名为HandleRopeUnwrap()的新方法。

 private void HandleRopeUnwrap() { } 

转到Update()并在最后添加对我们的新方法的调用。

 HandleRopeUnwrap(); 

尽管HandleRopeUnwrap()什么也不做,但是现在我们可以处理与从边缘分离的整个过程相关的逻辑。

正如您在本教程的第一部分中所记得的那样,我们将绳索缠绕位置存储在一个名为ropePositions的集合中,该集合是一个List<Vector2>集合。 每次绳索缠绕一条边缘时,我们都会在该集合中保留此包裹点的位置。

为了使过程更高效,如果集合中存储的位置数等于或小于1,我们将不会在HandleRopeUnwrap()执行任何逻辑。

换句话说,当ropePositions钩到起点并且其绳索尚未缠绕在边缘上时, ropePositions的数量将为1,并且我们将不会遵循ropePositions的处理逻辑。

将此简单的return添加到HandleRopeUnwrap()的顶部,以节省宝贵的CPU周期,因为每秒从Update()多次调用此方法。

 if (ropePositions.Count <= 1) { return; } 

添加新变量


在此新测试下,我们将添加一些尺寸和参考,以实现实现扭曲逻辑基础所需的不同角度。 将以下代码添加到HandleRopeUnwrap()

 // Hinge =       // Anchor =     Hinge // Hinge Angle =   anchor  hinge // Player Angle =   anchor  player // 1 var anchorIndex = ropePositions.Count - 2; // 2 var hingeIndex = ropePositions.Count - 1; // 3 var anchorPosition = ropePositions[anchorIndex]; // 4 var hingePosition = ropePositions[hingeIndex]; // 5 var hingeDir = hingePosition - anchorPosition; // 6 var hingeAngle = Vector2.Angle(anchorPosition, hingeDir); // 7 var playerDir = playerPosition - anchorPosition; // 8 var playerAngle = Vector2.Angle(anchorPosition, playerDir); 

这里有很多变量,因此我将解释它们中的每一个,并添加一个方便的插图以帮助理解它们的目的。

  1. anchorIndexropePositions集合中位于集合末尾两个位置的索引。 我们可以将其视为从子弹位置到绳索上两个位置的点。 在下图中,这是将挂钩连接到表面的第一个点。 当ropePositions新的包裹点填充ropePositions集合ropePositions ,该点将始终保持包裹点ropePositions相距两个位置。
  2. 铰链索引是存储当前铰链点的集合的索引; 换句话说,绳子当前缠绕在从条塞最接近绳子末端的点周围的位置。 它总是与ropePositions.Count - 1相距一个位置,这就是为什么我们使用ropePositions.Count - 1
  3. anchorPosition通过引用ropePositions集合中的anchorIndex位置anchorPosition计算的,并且是该位置的简单Vector2值。
  4. hingePosition通过引用ropePositions集合中hingeIndex索引的位置hingePosition计算的,并且是该位置的简单Vector2值。
  5. hingeDir是从anchorPosition定向到hingePosition的向量。 在以下变量中使用它来获取角度。
  6. 铰链角度-有用的辅助函数Vector2.Angle()在此处用于计算anchorPosition和铰链点之间的角度。
  7. playerDir是从anchorPosition指向playerDir当前位置(playerPosition)的向量
  8. 然后,使用锚点和玩家(子弹)之间的角度,计算玩家角度。


所有这些变量都是使用存储在ropePositions集合中的Vector2值存储的位置并将这些位置与其他位置或播放器(子弹)的当前位置进行比较来计算的。

用于比较的两个重要变量是hingeAnglehingeAngle

存储在hingeAngle角度中的值必须保持静态,因为它始终是从hingeAngle的两个“绳索折叠”处的点到hingeAngle的当前“绳索的折叠”之间的点之间恒定的角度,直到松开绳索或折叠后该点才移动将添加一个新的弯曲点。

playerAngle改变。 通过将这个角度与hingeAngle角度进行hingeAngle ,并检查hingeAngle是在该角的左侧还是右侧,我们可以确定是否应分离最靠近hingeAngle的当前折叠点。

在本教程的第一部分中,我们将折叠位置保存在名为wrapPointsLookup的字典中。 每次保存弯曲点时,我们都将弯曲点添加到字典中,位置为键,值为0。 但是,这个0值相当神秘,对吧?

我们将使用该值存储弹头相对于其与铰链点(当前折叠点最接近弹头)的角度的位置。

如果您将值指定为-1 ,则hingeAngle的角度( hingeAngle )小于铰链的角度( hingeAngle ),并且值为1时, hingeAngle 角度大于hingeAngle 角度。

由于我们将值保存在字典中,因此,每次将hingeAnglehingeAngle进行比较时,我们都可以了解到弹头是否刚刚超过了限制,然后就可以解开绳索了。

可以用不同的方式解释:如果刚检查了弹头的角度并且该角度小于铰链角度,但是最后一次将其保存在折弯点字典中时,将其标记为一个值,指示该点位于该拐角的另一侧,则应立即移除该点。 !

解开绳


请看下面的屏幕截图,并附上注释。 我们的弹头紧紧抓住岩石,向上摇摆,在上升的过程中将一根绳子缠绕在岩石的边缘。


您可能会注意到,在wrapPointsLookup不透明的最高挥杆位置,其当前最近的折叠点(标有白点)将存储在wrapPointsLookup词典中,值为1

在向下的过程中,当playerAngle变得小于hingeAngle (两条绿色的虚线)时,如蓝色箭头所示,将执行检查,如果折弯点的最后一个(当前)值为1 ,则应删除折弯点。

现在,让我们在代码中实现此逻辑。 但是在开始之前,让我们创建一个将用于展开的方法的空白。 因此,创建逻辑后,不会导致错误。

通过插入以下行来添加新的UnwrapRopePosition(anchorIndex, hingeIndex)方法UnwrapRopePosition(anchorIndex, hingeIndex)

 private void UnwrapRopePosition(int anchorIndex, int hingeIndex) { } 

完成此操作后,回到HandleRopeUnwrap() 。 在新添加的变量下,添加以下逻辑,该逻辑将处理两种情况: hingeAngle小于hingeAngle以及hingeAngle大于hingeAngle

 if (playerAngle < hingeAngle) { // 1 if (wrapPointsLookup[hingePosition] == 1) { UnwrapRopePosition(anchorIndex, hingeIndex); return; } // 2 wrapPointsLookup[hingePosition] = -1; } else { // 3 if (wrapPointsLookup[hingePosition] == -1) { UnwrapRopePosition(anchorIndex, hingeIndex); return; } // 4 wrapPointsLookup[hingePosition] = 1; } 

此代码应对应于上述第一种情况的逻辑说明(当hingeAngle < hingeAngle ),但也应处理第二种情况(当hingeAngle > hingeAngle )。

  1. 如果最接近hingeAngle的当前折叠点在hingeAngle < hingeAngle角度的点处的值为1 ,则我们删除该点并执行返回操作,以便该方法的其余部分不执行。
  2. 否则,如果弯曲点最后未标记为值1 ,但hingeAngle小于hingeAngle ,则分配-1
  3. 如果当前最靠近hingeAngle折叠点在hingeAngle > hingeAngle的点处为-1 ,则移除该点并返回。
  4. 否则,我们将折点字典中位于铰链位置的条目分配为1

此代码确保wrapPointsLookup字典始终处于更新状态,以确保当前折弯点的值(最接近wrapPointsLookup块)与当前相对于折弯点的wrapPointsLookup块角度匹配。

不要忘记,当凸角小于铰链角(相对于参考点)时,该值为-1;而当凸角大于铰链角时,该值为1。

现在,我们将在RopeSystem脚本中UnwrapRopePosition()代码, 代码将直接进行解耦,移动参考位置并为绳索距离值DistanceJoint2D分配新的距离值。 将以下行添加到以前创建的方法光盘中:

  // 1 var newAnchorPosition = ropePositions[anchorIndex]; wrapPointsLookup.Remove(ropePositions[hingeIndex]); ropePositions.RemoveAt(hingeIndex); // 2 ropeHingeAnchorRb.transform.position = newAnchorPosition; distanceSet = false; // Set new rope distance joint distance for anchor position if not yet set. if (distanceSet) { return; } ropeJoint.distance = Vector2.Distance(transform.position, newAnchorPosition); distanceSet = true; 

  1. 当前锚点的索引(绳索从子弹的第二位置)变为铰链的新位置,并且铰链的旧位置被移除(以前最靠近子弹的位置,现在我们“解开”)。 变量newAnchorPosition在绳索位置列表中分配了值anchorIndex 。 然后将其用于定位锚点的更新位置。
  2. 绳索接头RigidBody2D(DivistJoint2D绳索连接到该绳索接头)将其位置更改为锚点的新位置。 这确保了将子弹连接到DistanceJoint2D时,它在绳索上的平稳连续运动,并且此连接应允许它相对于新位置继续摆动,该新位置成为参考-换句话说,相对于绳索从其位置向下的下一个点。
  3. 然后,您需要更新distanceJoint2D距离值,以考虑从弹头到新参考点的距离的急剧变化。 如果尚未完成此操作,则将对distanceSet标志进行快速检查,并为该距离分配弹头与锚点的新位置之间的计算距离值。

保存脚本并返回到编辑器。 再次开始游戏,并观察当弹头超过每个弯曲点的阈值时绳索如何从边缘脱离!


尽管逻辑已经准备好了,但我们还是在将hingeAnglehingeAngleif (playerAngle < hingeAngle) )进行比较之前,向HandleRopeUnwrap()添加了一些辅助代码。

 if (!wrapPointsLookup.ContainsKey(hingePosition)) { Debug.LogError("We were not tracking hingePosition (" + hingePosition + ") in the look up dictionary."); return; } 

实际上,这不应该发生,因为当猫钩两次缠绕一条肋骨时,我们会重新定义并断开其连接,但是如果仍然发生这种情况,我们可以通过简单的return和错误消息轻松退出此方法。在控制台中。

此外,由于此,我们将更方便地处理这种限制情况; 此外,如果发生不必要的事情,我们会收到错误消息。

接下来要去哪里?


这里是本教程第二部分和最后一部分的完成项目的链接

祝贺您完成本教程系列! 当比较角度和位置时,一切都变得相当复杂,但是我们幸免于难,现在我们有了一个很棒的钩猫和绳索系统,可以将游戏中的物体缠绕起来。


您知道我们的Unity开发团队写过书吗? 如果不是,请查看Unity Games By Tutorials 。 该游戏将教您如何从头开始创建四个现成的游戏:

  • 两杆射手
  • 第一人称射击游戏
  • 塔防游戏(具有VR支持!)
  • 2D平台游戏

阅读本书之后,您将学习如何为Windows,macOS,iOS和其他平台创建自己的游戏!

本书既适合初学者,又适合希望将Unity技能提升到专业水平的人。 要掌握这本书,您需要具有编程经验(任何语言)。

Source: https://habr.com/ru/post/zh-CN415217/


All Articles