目录
GeneRope.cs 场景绳索生成类
HeroColliderController.cs 控制角色与单向平台是否忽略碰撞
HeroClampController.cs 控制角色攀爬
OnTriggerEnter2D方法
OnTriggerStay2D方法
OnTriggerExit2D方法
Update方法
开始攀爬
结束攀爬
Sensor_HeroKnight.cs 角色触发器
HeroKnight.cs 角色类
基于【Unity3D】实现横版2D游戏——单向平台(简易版)-CSDN博客
实现了单向平台后才能进行攀爬绳索到平台,否则攀爬到平台后会被阻挡,以及无法从平台往下攀爬。
GeneRope.cs 场景绳索生成类
GitHub - dbrizov/NaughtyAttributes: Attribute Extensions for Unity
上面是一个特性能够让脚本上出现一个按钮去点击执行我们的生成绳索方法
用法:[Button("Generate Rope")] 写在生成绳索方法头部。
绳索标签Rope
using System.Collections;
using NaughtyAttributes;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class GeneRope : MonoBehaviour
{
public Sprite top;
public Sprite middle;
public Sprite bottom;
public int ropeNodeCount;
public Vector2 ropeNodeSize;
List<GameObject> nodeList;
BoxCollider2D boxCollider2D;
[Button("Generate Rope")]
public void GenerateRope()
{
//清理
if (nodeList == null)
{
nodeList = new List<GameObject>();
}
else
{
for (int i = nodeList.Count - 1; i >= 0; i--)
{
DestroyImmediate(nodeList[i]);
}
nodeList.Clear();
}
Transform[] childrens = transform.GetComponentsInChildren<Transform>();
if (childrens != null && childrens.Length > 0)
{
for (int i = childrens.Length - 1; i >= 0; i--)
{
if (childrens[i].gameObject != this.gameObject)
{
DestroyImmediate(childrens[i].gameObject);
}
}
}
//生成绳索节点图片
for (int i = 0; i < ropeNodeCount; i++)
{
Sprite sprite;
if (i == 0)
{
//头部
sprite = top;
}
else if (i == ropeNodeCount - 1)
{
//尾部
sprite = bottom;
}
else
{
//中间
sprite = middle;
}
SpriteRenderer sp = new GameObject("node_" + i, typeof(SpriteRenderer)).GetComponent<SpriteRenderer>();
sp.sprite = sprite;
nodeList.Add(sp.gameObject);
sp.transform.SetParent(transform);
//从根节点向下延伸的绳索
sp.transform.localPosition = new Vector3(0, i * -ropeNodeSize.y - 0.5f, 0);
}
//生成绳索碰撞体(1个)
boxCollider2D = GetComponent<BoxCollider2D>();
if (boxCollider2D == null)
{
boxCollider2D = gameObject.AddComponent<BoxCollider2D>();
}
boxCollider2D.size = new Vector2(ropeNodeSize.x, ropeNodeSize.y * ropeNodeCount);
boxCollider2D.offset = new Vector2(0, boxCollider2D.size.y * -0.5f);
boxCollider2D.isTrigger = true;
//绳索标签(触发检测使用判定是否为绳索,处理攀爬逻辑)
gameObject.tag = "Rope";
}
}
HeroColliderController.cs 控制角色与单向平台是否忽略碰撞
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HeroColliderController : MonoBehaviour
{
BoxCollider2D heroCollider;
public bool isOn;
Collider2D m_Collision;
public bool isIgnoreCollision = false;
private void Awake()
{
heroCollider = GetComponent<BoxCollider2D>();
}
IEnumerator StartIgnoreCollision(Collider2D collider2D)
{
//Debug.Log("忽略碰撞");
//等待人物完整地达到平台上 或 平台下时 退出死循环
while (isOn)
{
yield return new WaitForEndOfFrame();
}
//Debug.Log("恢复");
Physics2D.IgnoreCollision(heroCollider, collider2D, false); //恢复
isIgnoreCollision = false;
isOn = false;
}
private void OnTriggerEnter2D(Collider2D collision)
{
//实现从下往上跳跃穿过单向平台效果
if (collision != null && collision.gameObject.tag == "SingleDirCollider" && !isOn)
{
//this.transform.position.y + 0.062f是人物minY
if (collision.GetComponent<SingleDirCollider>().maxY > (this.transform.position.y + 0.062f))
{
IgnoreCollision(collision);
}
}
}
private void OnCollisionStay2D(Collision2D collision)
{
//实现按下↓键位 让角色从单方向平台往下掉效果
if (Input.GetKeyDown(KeyCode.DownArrow) && collision.gameObject.tag == "SingleDirCollider")
{
IgnoreCollision(collision.collider);
}
}
public void IgnoreCollision(Collider2D collision)
{
isOn = true;
m_Collision = collision;
Physics2D.IgnoreCollision(heroCollider, collision); //这2个碰撞体互相忽略碰撞
isIgnoreCollision = true;
StartCoroutine(StartIgnoreCollision(collision));
}
public void ResumeCollider()
{
Physics2D.IgnoreCollision(heroCollider, m_Collision, false); //恢复
isOn = false;
isIgnoreCollision = false;
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision != null && collision.gameObject.tag == "SingleDirCollider" && isOn)
{
var singleDirCollider = collision.GetComponent<SingleDirCollider>();
//this.transform.position.y + 0.062f是人物minY, this.transform.position.y + 1.31f是人物maxY
//当人物完整地位于平台之上 或 之下时
if (singleDirCollider.maxY < (this.transform.position.y + 0.062f) || singleDirCollider.minY > (this.transform.position.y + 1.31f))
{
//协程死循环退出
isOn = false;
}
}
}
}
HeroClampController.cs 控制角色攀爬
OnTriggerEnter2D方法
当某个触发器检测到单向平台,持有它 并 triggerCount计数+1
OnTriggerStay2D方法
当角色检测到绳索时,角色底部在绳索顶部之上,需要按↓键才开始往下爬,此时需要立刻将角色底部瞬移到绳索顶部之下,避免与结束攀爬条件3冲突即
//3.接触单向平台 且人物在单向平台之上时 离开攀爬
lastIsTouchSingleDirPlatform = (singleDirCollider.maxY) < (heroKnight.transform.position.y + 0.062f);
角色底部在绳索顶部之下,需要按↑键才开始往上爬。
OnTriggerExit2D方法
检测到某个触发器离开单向平台时,triggerCount计数-1,当triggerCount为0时,需要进行结束攀爬条件2判定
//2.接触单向平台 且人物在单向平台之上时 离开攀爬
lastIsTouchSingleDirPlatform = (singleDirCollider.maxY) <= (heroKnight.transform.position.y + 0.062f);
并且将持有的单向平台脚本置空 singleDirCollider = null;
Update方法
检查到攀爬中,会使用角色类heroKnight.MoveY移动角色进行匀速运动上下攀爬。
进行结束攀爬条件1,2种情况:
1、isGrounds && !singleDirCollider :在地面且没有接触中的单向平台,结束攀爬
2、isGrounds && singleDirCollider && (singleDirCollider.maxY) <= (heroKnight.transform.position.y + 0.062f):在地面且接触单向平台中且角色在平台之上,结束攀爬。
开始攀爬
需要立即忽略重力影响以及接触中的单向平台碰撞体,否则会发生异常。
结束攀爬
需要立即恢复重力以及碰撞体
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HeroClampController : MonoBehaviour
{
//英雄类对象 控制移动
HeroKnight heroKnight;
//是否处于攀爬状态
public bool isOn;
//仅用于观察、调试参数
public bool heroGround;
//仅用于观察 绳索Y参数
public float ropeY;
//攀爬速度
public float speed = 2f;
//攀爬达到的单向平台
private SingleDirCollider singleDirCollider;
private int triggerCount = 0; //可能会有多个触发检测到平台,需要用计数形式来检测 是否完全离开平台
//一个控制是否忽略角色与平台碰撞的控制器(攀爬开始 检测到平台会立刻忽略碰撞 防止角色和平台发生碰撞, 以及结束攀爬时立即恢复碰撞 让玩家站在上面)
private HeroColliderController heroColliderController;
//仅用于观测数据 调试 : 上次是否触碰到单向平台 并恰好离开了平台 标记
public bool lastIsTouchSingleDirPlatform;
void Start()
{
heroKnight = GetComponent<HeroKnight>();
heroColliderController = GetComponent<HeroColliderController>();
}
public float InputY
{
get
{
return Input.GetAxis("Vertical");
}
}
private void Update()
{
heroGround = heroKnight.IsGrounded();
if (isOn)
{
//攀爬移动
float inputY = InputY;
heroKnight.MoveY(inputY * speed * Time.deltaTime);
//Debug.Log("攀爬:" + (inputY * speed * Time.deltaTime) + ", posY:" + (heroKnight.transform.position.y + 0.062f));
bool isGrounds = heroKnight.IsGrounded();
//1.接触地面 离开攀爬
//2.接触单向平台 且人物在单向平台之上时 离开攀爬
bool isTouchGround = isGrounds && !singleDirCollider;
bool isTouchSingleDirPlatform = isGrounds && singleDirCollider && (singleDirCollider.maxY) <= (heroKnight.transform.position.y + 0.062f);
if (isTouchGround || isTouchSingleDirPlatform)
{
EndClamp();
//Debug.LogError("攀爬结束 isOn:" + isOn + ", isTouchGround:" + isTouchGround + ", isTouchSingleDirPlatform:" + isTouchSingleDirPlatform
// + " singleDirCollider.maxY:" + singleDirCollider?.maxY + ", posY:" + (heroKnight.transform.position.y + 0.062f));
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
//触发检测到平台 持有它
if (collision.tag == "SingleDirCollider")
{
triggerCount++;
singleDirCollider = collision.transform.GetComponent<SingleDirCollider>();
}
}
private void OnTriggerStay2D(Collider2D collision)
{
if (isOn && singleDirCollider != null)
{
//3.接触单向平台 且人物在单向平台之上时 离开攀爬
lastIsTouchSingleDirPlatform = (singleDirCollider.maxY) < (heroKnight.transform.position.y + 0.062f);
//Debug.LogError("离开时: " + (singleDirCollider.maxY) + ", HeroY:" + (heroKnight.transform.position.y + 0.062f));
if (lastIsTouchSingleDirPlatform)
{
EndClamp();
//Debug.LogError(">>>>> 3333 攀爬结束 isOn:" + isOn + ", isTouchSingleDirPlatform:" + lastIsTouchSingleDirPlatform
// + " singleDirCollider.maxY:" + singleDirCollider?.maxY + ", posY:" + (heroKnight.transform.position.y + 0.062f));
}
}
if (collision.tag == "Rope" && !isOn)
{
float inputY = InputY;
bool hasInput = Mathf.Abs(inputY) > 0;
bool isGrounds = heroKnight.IsGrounded();
bool isValidInput = !isGrounds; //非站立地面则允许攀爬
float heroY = heroKnight.transform.position.y + 0.062f;
float ropeY = collision.transform.position.y;
float deltaY = Mathf.Abs(heroY - ropeY); //绳索顶部和英雄底部的差值
//若站立地面上
if (hasInput && isGrounds)
{
this.ropeY = ropeY;
if (heroY > ropeY)
{
isValidInput = inputY < 0; //角色位于绳索上方,按↓键才能攀爬
}
else
{
isValidInput = inputY > 0; //角色位于绳索下方,按↑键才能攀爬
}
}
//Debug.Log("isValidInput:" + isValidInput + " inputY :" + inputY + ", heroY:" + heroY + ", ropeY:" + ropeY);
if (hasInput && isValidInput) //可以检测角色中心点x是否在绳索范围内才进入攀爬状态
{
var hero = heroKnight.transform;
var rope = collision.transform;
int dir = (inputY > 0 ? 0 : -1); //按↑时不需要偏移Y值,按↓时进行偏移
//Debug.LogError("攀爬开始 heroY:" + heroY + ", ropeY:" + ropeY + ", inputY:" + inputY + ", isGrounds:" + isGrounds);
//当角色从绳索顶部往下爬时,需要增加一个Y轴偏移 使得角色底部位于绳索顶部之下 避免发生临界点问题
//(避免:开始攀爬 又会检测到物体在绳索上而结束攀爬的情况)
float offsetY = 0f;
if (heroY > ropeY)
{
offsetY = -deltaY - 0.01f;
}
hero.position = new Vector3(rope.position.x, hero.position.y + offsetY, 0);
isOn = true;
heroKnight.isClamp = true;
heroKnight.BlockGravity(true); //禁用重力影响
if (singleDirCollider != null)
{
heroColliderController.IgnoreCollision(singleDirCollider.GetComponent<BoxCollider2D>()); //主动忽略碰撞体
}
//heroKnight.MoveY(inputY * speed * Time.deltaTime);
//Debug.LogError("攀爬开始 isOn:" + isOn + ", heroY:" + (hero.position.y + 0.062f) + ", dir * deltaY:" + dir * deltaY);
}
}
//Debug.Log("heroKnight.transform.position.y + 0.062f:" + (heroKnight.transform.position.y + 0.062f));
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.tag == "SingleDirCollider" && singleDirCollider != null && collision.gameObject == singleDirCollider.gameObject)
{
triggerCount--;
if (triggerCount <= 0)
{
if (isOn)
{
//2.接触单向平台 且人物在单向平台之上时 离开攀爬
lastIsTouchSingleDirPlatform = (singleDirCollider.maxY) <= (heroKnight.transform.position.y + 0.062f);
//Debug.LogError("离开时: " + (singleDirCollider.maxY) + ", HeroY:" + (heroKnight.transform.position.y + 0.062f));
if (lastIsTouchSingleDirPlatform)
{
EndClamp();
//Debug.LogError(">>>>> 2222 攀爬结束 isOn:" + isOn + ", isTouchSingleDirPlatform:" + lastIsTouchSingleDirPlatform
// + " singleDirCollider.maxY:" + singleDirCollider?.maxY + ", posY:" + (heroKnight.transform.position.y + 0.062f));
}
}
singleDirCollider = null;
}
}
//还有其他的情况 退出攀爬状态的,比如攀爬中允许跳出去之类的 就还要写类似计数形式的 检测是否完全离开了绳索,再结束攀爬状态isOn = false
//if (collision.tag == "Rope")
//{
// isOn = false;
//}
}
/// <summary>
/// 结束攀爬
/// </summary>
private void EndClamp()
{
isOn = false;
heroKnight.isClamp = false;
heroKnight.BlockGravity(false); //恢复重力影响
if (heroColliderController != null)
{
heroColliderController.ResumeCollider(); //恢复碰撞体
}
}
}
Sensor_HeroKnight.cs 角色触发器
主要是忽略对Rope绳索层的检测,否则角色会认为绳索是地面或障碍物。
using UnityEngine;
using System.Collections;
public class Sensor_HeroKnight : MonoBehaviour
{
public int m_ColCount = 0;
public float m_DisableTimer;
public bool m_State;
private void OnEnable()
{
m_ColCount = 0;
}
public bool State()
{
if (m_DisableTimer > 0)
{
m_State = false;
return m_State;
}
m_State = m_ColCount > 0;
return m_State;
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.tag != "Rope")
{
m_ColCount++;
}
}
void OnTriggerExit2D(Collider2D other)
{
if (other.tag != "Rope")
{
m_ColCount--;
}
}
void Update()
{
m_DisableTimer -= Time.deltaTime;
}
public void Disable(float duration)
{
m_DisableTimer = duration;
}
}
HeroKnight.cs 角色类
如下是部分代码新增isClamp是否攀爬中标记,Move和Roll判定要加上非攀爬中才允许进行,新增MoveY(攀爬时上下移动使用)、BlockGravity(禁用重力影响 攀爬时会禁用)、IsGrounded(获取是否真正在地面上)
public bool isClamp;
// Move
if (!m_rolling && !isClamp )
m_body2d.velocity = new Vector2(inputX * m_speed, m_body2d.velocity.y);
// Roll
else if (Input.GetKeyDown("left shift") && !m_rolling && !isClamp)
{
m_rolling = true;
m_animator.SetTrigger("Roll");
m_body2d.velocity = new Vector2(m_facingDirection * m_rollForce, m_body2d.velocity.y);
}
public void MoveY(float y)
{
m_body2d.MovePosition(new Vector2(transform.position.x, transform.position.y + y));
}
public void BlockGravity(bool isStatic)
{
float oldScale = m_body2d.gravityScale;
float newScale = isStatic ? 0f : 1f;
if (Mathf.Abs(oldScale - newScale) > 0.0001)
{
m_body2d.gravityScale = newScale;
m_body2d.velocity = new Vector2(0, 0);
}
}
public bool IsGrounded() { return m_groundSensor.State(); }