这节讲讲类似马里奥,盗贼遗产,这些类似的游戏人物基本的控制方式。这里我用Prime31这个公司的插件改了一个比较小的控制脚本,博客结尾会给大家一份源码,先简单的讲一下2D人物控制器的原理,2D控制器可不像untiy 3D控制器那样,untiy官方帮我们封装了。所以这里就只能自己去写了,先贴出一张图。
图中的场景时源插件中的场景,我就直接拿来用免得自己去摆了,图中有3中不同的障碍物,方块代表玩家,1号障碍物是无法穿过的,2号可以穿过,3号是一个简单的坡,玩家可以从2好障碍物的下面直接跳上2,如果站在1号障碍物的下面是无法跳上1号障碍物。站在2号障碍物上可以按跳下键直接从2上面跳下,站在1号障碍物上无法从上面跳下。3号是一个长坡,如果坡的陡峭程度超过了玩家的最大爬坡角度的话玩家是不能爬上去的,有的坡可以像2号障碍物那样可以跳上和跳下。基本的介绍完了,接下来就是具体讲讲怎么实现了,这种控制器的核心就是射线检测。假如玩家受到重力的作用(这里的重力我们用一个常量来模拟,不用刚体自带的重力)开始往下落它遇到地面的时候会停止下落,那么首先我们要做的就是判断落地,我们在玩家盒子碰撞器的四周添加16个射线发射点,每个边有4个发射点,当我们速度y方向的值为正时表示我们在向上运动,反之在向下运动,为0就表示不动了,当y<0时,我们速度为velocity,velocity在y方向的分量为velocity.y,那么玩家下帧的偏移量我们定义为delta=velocity*Time.delatime;我们在玩家碰撞器的下边依次均匀分布在这条边上(考虑到碰撞器紧贴着其他碰撞器,所以我们的发射点在y轴上减去一点偏移量,我们把这个偏移量定义为skin,即皮肤),发射点问题解决了,接下来就是方向,距离,层的确定问题,如果速度的方向为下,方向就是-vector2.up,距离就是我们下帧的偏移量,层就是default层,1号障碍物的层为default,2号障碍物的层我们定义为OneWayPlatform,当velocity.y>0表示玩家正在向上运动,这是我们射线检测的点就是碰撞器向上的那个边了,四个发射点的位置在y轴上的分量同样依次减去skin偏移量,射线的方向为vector2.up,射线的距离为当前帧的偏移量,层我们忽略OneWayPlatform,当velocity.x>0时表示玩家往右移动,同时发射点取右边碰撞器的边,同样也需要同时减去skin,射线的方向为vector2.right,距离为当前帧的偏移量,层为default层。velocity.x<0以相反的情况推理就行了,代码如下:
using UnityEngine; namespace Assets.LiuDoubi { [RequireComponent(typeof(BoxCollider2D), typeof(Rigidbody2D))] public class SampleCharacterCtr2D : MonoBehaviour { public struct CharacterColState2D { public bool Right; public bool Left; public bool Top; public bool Bottom; public bool IsColision { get { return Right || Left || Top || Bottom; } } public void Reset() { Right = Left = Top = Bottom = false; } } [SerializeField] private float _skin = 0.1f;//皮肤的厚度,一般定义0.001-0.1,建议在0.00级别。 [SerializeField] private int _hraycount;//在x轴方向射线个数,我们定义为4个,同时y轴方向的射线个数也是4个 [SerializeField] private int _vraycount; [SerializeField] private LayerMask _obstacleMask;//1号障碍物的层标识,default的层数字为1,所在的实际层为2<<层数 [SerializeField] private LayerMask _onewayPlatforms;//2号障碍物的层标识 [SerializeField] private float _slopeLimit;//玩家所能爬的最陡坡度 public AnimationCurve slopeSpeedMultiplier = new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(90f, 0f)); private BoxCollider2D _boxCollider2D; private CharacterColState2D _colState2D;//碰撞信息,分别存储了4个方向的碰撞信息,分别为top,bottom,left,right private Vector3 _bottomright;//底边射点的右起始点,后面的点加上一点的偏移量即可求得,下面依次类推 private Vector3 _bottomleft; private Vector3 _topright; private Vector3 _topleft; private LayerMask _curMask;//最后玩家实际需要检测的层 public Vector3 Velocity;//玩家的速度 public bool IgnoreOneWayPlatforms;//表示是否忽略OneWayPlatforms private bool _lastFramIsGround; public bool IsGround { get { return _colState2D.Bottom; } } private void Awake() { _boxCollider2D = this.transform.GetComponent<BoxCollider2D>();//先缓存一下玩家的盒子碰撞器 GetRayOffset(); } public void Move(Vector3 delta)//该方法为该组件对外的方法。同时也是主要方法。如果当前帧没碰到碰撞器,那下帧情况我们不是很清楚,所以我们的做法就是每帧都去检测,同样每帧的碰撞信息都应该清除 { _curMask = _obstacleMask; _lastFramIsGround = IsGround;//判断上一帧玩家是不是在地上,因为没帧都在检测,所以我们需要保存上一帧的玩家碰撞信息。 _colState2D.Reset();//碰撞信息重新清除 RecalculateRayOrigin();//重新计算射线其实点,每帧玩家的位置信息都在发生变化,所以每帧都需重新计算。 if (Mathf.Abs(delta.x) > 0.001f)//如果在x方向偏移不为0,对其进行计算,如果y轴上的偏移不为0同样也需对其进行计算。这2个方法的作用就是返回一个偏移通过delta传出去,ref 和 out表示值类型的参数按引用传递,本质都是传的地址。区别我就不说了。 MoveHorizontal(ref delta); if (Mathf.Abs(delta.y) > 0.001f) MoveVertical(ref delta); delta.z = 0; this.transform.Translate(delta, Space.World); if (Time.deltaTime > 0f) Velocity = delta / Time.deltaTime; IgnoreOneWayPlatforms = false; } private void MoveHorizontal(ref Vector3 delta) { bool isrgiht = delta.x > 0;//判断是不是向右 Vector2 raydir = isrgiht ? Vector2.right : -Vector2.right;//如果向右的话,那么射线的方向就是向右的。 Vector2 rayorigin = isrgiht ? _bottomright : _bottomleft;//如果向右的话,起始点我们从右下边,反之就是左下边取。 float rayDistance = Mathf.Abs(delta.x) + _skin;射线的方向是偏移量在x轴上的分量加上皮肤的厚度。 for (int i = 0; i < _hraycount; i++) { var originpos = new Vector2(rayorigin.x, rayorigin.y + i * _vrayoffset);//依次求出射点的位置。 RaycastHit2D hit; hit = Physics2D.Raycast(originpos, raydir, rayDistance, _obstacleMask); if (hit) { if (i == 0 && HandleHorizontalSlope(ref delta, Vector2.Angle(hit.normal, Vector2.up)))//如果玩家是在爬坡的话,并且只有0号射点才能检测到障碍物的话,那么我们不用计算下面的了。 { break; } delta.x = hit.point.x - originpos.x; if (isrgiht)//如果检测到了障碍物的话,并且是在右边话,表示玩家的朝向有一个障碍物 { delta.x -= _skin; _colState2D.Right = true; } else { delta.x += _skin; _colState2D.Left = true; } break; } } }//该方法的作用是,假如我们离障碍物有0.1,如果我们下一帧移动偏移量为0.2的话,如果我们还是按照0.2进行移动话,那么玩家就和障碍物重叠了,如果这时我们发现重叠了,然后又把玩家移动-0.1的偏移的话,在短暂的时间内,一般玩家是可以看出来的,所以我们的做法应该在这一帧只移动0.1.。同时下面的方法都是一样的,只有上坡的时候检测可能有点区别。 private void MoveVertical(ref Vector3 delta) { bool isdown = delta.y < 0; Vector2 raydir = isdown ? -Vector2.up : Vector2.up; Vector2 rayorigin = isdown ? _bottomleft : _topleft; float rayDistance = Mathf.Abs(delta.y) + _skin; if (isdown && !_lastFramIsGround) _curMask |= _onewayPlatforms; if (_lastFramIsGround && IgnoreOneWayPlatforms) _curMask &= ~_onewayPlatforms; for (int i = 0; i < _vraycount; i++) { var originpos = new Vector2(rayorigin.x + i * _hrayoffset, rayorigin.y); RaycastHit2D hit; hit = Physics2D.Raycast(originpos, raydir, rayDistance, _curMask); if (hit) { delta.y = hit.point.y - originpos.y; if (isdown) { delta.y += _skin; _colState2D.Bottom = true; } else { delta.y -= _skin; _colState2D.Top = true; } break; } } } private bool HandleHorizontalSlope(ref Vector3 delta, float angle) { if (Mathf.RoundToInt(angle) == 90) return false; if (angle < _slopeLimit) { if (delta.y < 0.05f) { var slopeModifier = slopeSpeedMultiplier.Evaluate(angle); delta.x *= slopeModifier; delta.y = Mathf.Abs(Mathf.Tan(angle * Mathf.Deg2Rad) * delta.x); var isGoingRight = delta.x > 0; var ray = isGoingRight ? _bottomright : _bottomleft; RaycastHit2D raycastHit; //if (_lastFramIsGround) raycastHit = Physics2D.Raycast(ray, delta.normalized, delta.magnitude, _curMask); if (raycastHit) { delta = (Vector3)raycastHit.point - ray; if (isGoingRight) delta.x -= _skin; else delta.x += _skin; } //_isGoingUpSlope = true; _colState2D.Bottom = true; } } return true; } private void RecalculateRayOrigin()//重新计算射线的各个位置的起始位置 { float xoffset = _boxCollider2D.size.x * this.transform.localScale.x / 2; float yoofset = _boxCollider2D.size.y * this.transform.localScale.y / 2; _topleft = transform.position + new Vector3(-xoffset + _skin, yoofset - _skin, 0); _bottomleft = transform.position + new Vector3(-xoffset + _skin, -yoofset + _skin, 0); _topright = transform.position + new Vector3(xoffset + -_skin, yoofset - _skin, 0); _bottomright = transform.position + new Vector3(xoffset + -_skin, -yoofset + _skin, 0); } private float _hrayoffset; private float _vrayoffset; private void GetRayOffset()//计算每边射点与射点之间的距离。 { var xdis = _boxCollider2D.size.x * this.transform.localScale.x - 2 * _skin; var ydis = _boxCollider2D.size.y * this.transform.localScale.y - 2 * _skin; _hrayoffset = xdis / _hraycount; _vrayoffset = ydis / _vraycount; } } }
这样讲的好麻烦了,总之一句话,move方法中 MoveHorizontal(ref delta); MoveVertical(ref delta);就是计算玩家下一帧需要移动的偏移量。这里给出了2套控制代码,一套是Prime31,另外一套是自己写,但是大家可以先学习prime31这个插件。然后去写自己角色控制器,我写这些只是帮助大家理解角色控制器。http://pan.baidu.com/s/1hrGNgiS
如果有什么不懂的可以私聊,qq:1850761495