前言
最近在自己做开发的时候突然想在2D游戏做一个扔出锁链的效果,但是在网上感觉相关的实现教程都略深奥了一点(=v=、),于是自己研究了一下,也借鉴了社区里的一些讲解和教程,终于搞明白了实现方法,在这里和大家分享一下~
HingeJoint2D铰链关节的使用要点
HingeJoint2D(铰链关节)是Unity自带的物理关节组件之一,主要用于实现两个刚体之间相互勾连旋转的效果(比如钟摆的效果、行星绕恒星旋转的效果等等),除此之外,Unity中还带有FixedJoint(固定关节)、SpringJoint(弹簧关节)、CharacterJoint(角色关节)等多种关节组件,具体用法可以阅读官方文档查看,这里就不详细介绍了。
这一节主要讲一些在HingeJoint2D组件在实现锁链过程中需要注意的一些用法。 在本节中HingeJoint2D面板上主要需要注意的属性有三个:
-
Connected Rigidody:连接的目标刚体,选定物体后本物体会绕着目标物体的锚点为轴心转动(公转,可以类比钟摆的固定点) -
Anchor:自身锚点,自身在与目标物体的连接之外若受到其他的力则自身会绕着自身锚点旋转(自转的轨迹中心),默认在自身刚体中心,采用local坐标系,以物体本身位置为(0,0)计算 -
Connected Anchor:目标物体锚点,可以设置公转的中心点位于目标刚体的哪个位置,默认在目标刚体中心,采用local坐标系,以物体本身位置为(0,0)计算 -
Auto Configure Connected Anchor:如果勾选这一项的话Connected Anchor始终保持在目标物体RigidBody的位置,无法修改坐标值
除此之外在锁链的实现方法中还需要注意:
-
锁片的长宽尺寸大小:在设置锁链间距的时候需要计算锁链大小,可以通过SpriteRender组件中的bound变量(变量类型为Vector2)获取当前scale下Sprite的实际长宽; -
由于anchor采用local坐标系,因此anchor坐标的数值不受transform中scale的影响,而是始终基于Sprite原本大小计算,因此在计算两个锁片之间的最大长度时,应该使用 最大距离=锁环1的anchor坐标长度 * 锁环1的scale +锁环2的anchor坐标长度 * 锁环2的scale
2D锁链的实现方案
虽然Unity中很难实现完全的柔性材质,但是锁链本质上是由多个环状链节构成的刚体组合。因此实现锁链的关键在于模拟单个链节之间的勾连效果和旋转关系。
基于HingeJoint2D模拟刚体间转轴施力的效果,我们可以通过以下方案模拟链节之间的相互受力:
-
对每个单个链节的尾部添加HingeJoint2D,并将该链节的前一个链节的头部作为相互链接的影响对象(Connected Anchor),以此模拟锁链运动端对锁链固定端的施力作用,如下图所示: -
其中锁链的起点链接在世界坐标系下事先设置的起点坐标(Connect RigidBody设为空): -
尾部每生成一个新的锁片,将其尾部一端链接到移动端的位置坐标,头部一端与前一个生成的链节的尾部形成铰链关节的指定关系: 由此通过HingeJoint2D的单向传递性设置实现锁链中链节相互施力、相互影响的效果。
实现代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ChainController : MonoBehaviour
{
public class Node {
public HingeJoint2D headJoint, tailJoint;
public GameObject node;
public Node(GameObject self)
{
node = self;
headJoint = node.GetComponents<HingeJoint2D>()[0];
tailJoint = node.GetComponents<HingeJoint2D>()[1];
}
}
public GameObject nodeFront, nodeSide;
List<Node> chain;
int maxLength;
Vector3 headPos, tailPos;
float nodeWidth, nodeHeight;
float anchorBios;
float scale = 0.3f;
private void Awake()
{
anchorBios = 0.55f;
maxLength = 15;
chain = new List<Node>();
}
void Start()
{
tailPos = new Vector3(0, 0, 0);
headPos = new Vector3(0, 0, 0);
nodeWidth = nodeFront.GetComponent<SpriteRenderer>().bounds.size.x;
}
void FixedUpdate()
{
if (Input.GetMouseButtonDown(0))
{
tailPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
tailPos = new Vector3(tailPos.x, tailPos.y, 0);
}
if (Input.GetMouseButton(0))
{
headPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
headPos = new Vector3(headPos.x, headPos.y, 0);
FixChainList();
}
if (Input.GetMouseButtonUp(0))
{
}
}
int nodeCount;
void FixChainList(){
float tempCount = ((headPos - tailPos).magnitude - anchorBios * scale * 2) / (anchorBios * scale * 2) + 1;
nodeCount = (int)tempCount;
nodeCount = nodeCount < 0 ? 0 : nodeCount;
nodeCount = nodeCount > maxLength ? maxLength : nodeCount;
headPos = tailPos + (headPos - tailPos).normalized * (anchorBios * scale * 2 * nodeCount+0.0005f);
while (chain.Count < nodeCount)
{
AddNodeToChain(chain);
}
while (chain.Count > nodeCount)
{
DeleteHeadNode(chain);
}
if (chain.Count > 0)
{
chain[chain.Count - 1].headJoint.enabled = true;
chain[chain.Count - 1].headJoint.connectedAnchor = headPos;
}
}
GameObject newNode;
void AddNodeToChain(List<Node> list)
{
float theta = Mathf.Atan2(headPos.y - tailPos.y, headPos.x - tailPos.x) + Mathf.PI;
if (list.Count == 0)
{
newNode = Instantiate(nodeFront, this.transform).gameObject;
newNode.transform.position = new Vector3(tailPos.x - anchorBios * scale * Mathf.Cos(theta), tailPos.y - anchorBios * scale * Mathf.Sin(theta), 0);
newNode.transform.rotation = Quaternion.Euler(0, 0, theta * Mathf.Rad2Deg);
list.Add(new Node(newNode));
list[0].tailJoint.autoConfigureConnectedAnchor = false;
list[0].tailJoint.connectedAnchor = tailPos;
list[0].headJoint.autoConfigureConnectedAnchor = false;
list[0].headJoint.connectedAnchor = headPos;
newNode.SetActive(true);
return;
}
newNode = list.Count % 2 == 0 ? nodeFront : nodeSide;
Vector3 temp = list[list.Count - 1].node.transform.position - new Vector3(anchorBios * scale * 2 * Mathf.Cos(theta), anchorBios * scale * 2 * Mathf.Sin(theta), 0);
list[list.Count - 1].headJoint.enabled = false;
newNode = Instantiate(newNode, temp, Quaternion.Euler(0, 0, theta * Mathf.Rad2Deg)).gameObject;
newNode.transform.parent = this.transform;
list.Add(new Node(newNode));
list[list.Count - 1].tailJoint.connectedBody = list[list.Count - 2].node.GetComponent<Rigidbody2D>();
list[list.Count - 1].tailJoint.autoConfigureConnectedAnchor = false;
list[list.Count - 1].tailJoint.connectedAnchor = new Vector2(-1 * anchorBios, 0);
list[list.Count - 1].headJoint.connectedBody = null;
list[list.Count - 1].headJoint.autoConfigureConnectedAnchor = false;
list[list.Count - 1].headJoint.connectedAnchor = headPos;
newNode.SetActive(true);
}
void DeleteHeadNode(List<Node> list)
{
Node delete = list[list.Count - 1];
list.Remove(delete);
GameObject.Destroy(delete.node);
if (list.Count > 0)
{
list[list.Count - 1].headJoint.connectedBody = null;
list[list.Count - 1].headJoint.autoConfigureConnectedAnchor = false;
list[list.Count - 1].headJoint.connectedAnchor = list[list.Count - 1].node.transform.InverseTransformPoint(headPos);
chain[chain.Count - 1].headJoint.enabled = true;
}
}
void ClearAllChain()
{
while (chain.Count > 0)
{
DeleteHeadNode(chain);
}
}
}
实现效果
锁链的摆动效果:
拖动锁链自动生成:
初步的物理效果:
结语
这里只是简单考虑了一下锁链的基础物理逻辑和实现,锁链这件表现上还有很多需要注意的细节要处理,后面有时间应该会继续研究一下碰撞等更加细节的表现,希望这些知识对各位有帮助,谢谢~!
|