最近在自己做开发的时候突然想在2D游戏做一个扔出锁链的效果,但是在网上感觉相关的实现教程都略深奥了一点(=v=、),于是自己研究了一下,也借鉴了社区里的一些讲解和教程,终于搞明白了实现方法,在这里和大家分享一下~
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
虽然Unity中很难实现完全的柔性材质,但是锁链本质上是由多个环状链节构成的刚体组合。因此实现锁链的关键在于模拟单个链节之间的勾连效果和旋转关系。
基于HingeJoint2D模拟刚体间转轴施力的效果,我们可以通过以下方案模拟链节之间的相互受力:
对每个单个链节的尾部添加HingeJoint2D,并将该链节的前一个链节的头部作为相互链接的影响对象(Connected Anchor),以此模拟锁链运动端对锁链固定端的施力作用,如下图所示:
尾部每生成一个新的锁片,将其尾部一端链接到移动端的位置坐标,头部一端与前一个生成的链节的尾部形成铰链关节的指定关系:
由此通过HingeJoint2D的单向传递性设置实现锁链中链节相互施力、相互影响的效果。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//专门设置一个父物体来管理整条锁链
public class ChainController : MonoBehaviour
{
//单个链节的数据结构,用来引用链节对应的物体和Joint组件
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;//锚点偏移Sprite中心的横向距离(初步定是0.55f)
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))
{
//ClearAllChain();
}
}
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);//预制体的Active设置为false,确保设置位置后在再显示
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);//预制体的Active设置为false,确保设置位置后在再显示
//双向设置也不会影响锁链的运动效果
//list[list.Count - 2].headJoint.connectedBody = list[list.Count - 1].node.GetComponent(); ;
//list[list.Count - 2].headJoint.autoConfigureConnectedAnchor = false;
//list[list.Count - 2].headJoint.connectedAnchor = new Vector2(anchorBios, 0);
}
//锁链缩短时优先删除移动端链节,并将前一个链节链接到移动端位置
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);
}
}
}
拖动锁链自动生成:
这里只是简单考虑了一下锁链的基础物理逻辑和实现,锁链这件表现上还有很多需要注意的细节要处理,后面有时间应该会继续研究一下碰撞等更加细节的表现,希望这些知识对各位有帮助,谢谢~!