这篇大致是优化了一下房间门和墙壁的逻辑,接着增加了人物逻辑,然后增加了切换房间的同时切换摄像机位置。部分逻辑也参考了其他前辈的资料。
首先是房间门,因为免费素材比较难找,游戏本身也是致敬以撒的,因此就直接沿用以撒的美术资源了(像素风横版素材比较多,会有跳跃之类的素材,但是2.5D不太用得到)。
先讲一下人物逻辑吧,做了最基础的移动和动画,使用资源的时候发现以撒的人物脑袋和身子是分开的(且脑袋有很多不同的素材,因为吃了不同道具会改变形态),因为它的攻击逻辑是上下左右键往对应方向发射子弹,然后移动方向和攻击方向可以分开,且攻击发射的子弹会有物理引擎(可以甩狙,且弹道不是水平的,会抛物线形式下坠),于是可以创建一个prefab,命名为player,然后将脑袋和身子单独作为子物体拖拽上去,然后分别给他们挂载Animator,制作不同动画。
身子的动画状态机切换如下,上下可以切换到左右,而左右无法切换到上下是因为斜着走,斜着走的时候虽然位移是斜着移动,但是动画还是采用左右移动的动画,如果两者可以自由切换动画就会抽搐,看个人表现需求了,这里注意将动画切换的exit time取消勾选,下方属性归零,不然动画切换会有延迟,看起来很不舒服。
通过站立模式可以切换到左右或者上下移动,上下移动是不需要区分的,左右移动可以通过输入的moveInput.x的正负来判断,通过开关Spirte Renerder中的filp.x来处理画面上的显示。
void UpdateMovement()
{
var h = Input.GetAxis("Horizontal");
var v = Input.GetAxis("Vertical");
moveInput = h * Vector2.right + v * Vector2.up;
//归一化,这样斜着走的速度不会超过移动速度。
if (moveInput.magnitude > 1f)
{
moveInput.Normalize();
}
myRigidbody.velocity = moveInput * Speed * Time.deltaTime;
}
void UpdateAnimator()
{
//身体左右翻转
if (moveInput.x < 0) { body.flipX = true; }
if (moveInput.x > 0) { body.flipX = false; }
bodyAnimation.SetFloat("UpAndDown", Mathf.Abs(moveInput.y));
bodyAnimation.SetFloat("RightAndLeft", Mathf.Abs(moveInput.x));
}
头部的动画逻辑也是类似的,甚至更简单,这边涉及到子弹发射的逻辑,当按下不同方向的方向键,再触发动画,因此只需要按下对应键修改bool值,触发动画即可。
然后是房间和门的逻辑,因为这张素材是一整张背景,地板和墙壁是连起来的,门是另外的素材,所以我们直接创建一个basic room然后将四边的门贴上去,通过代码判断该显示哪几个方向的门,此处逻辑上篇文章讲过了,就不细说了,主要讲一下room中的collider设置。
墙壁肯定是要设置Collider的,不然玩家可以直接穿过去,然后门的结构稍微复杂一点,分为没有门(有碰撞体),有门但是关闭(有碰撞体),有门且打开(有触发器),理清楚之后就可以写出不同情况下对于的enable状态,然后有门且打开时,会触发额外的事件:
通过player身上的触发器,判断tag是否是门,然后再判断触发器的名字,来调用房间管理器中的切换房间逻辑(这里设置了GameManager类储存游戏中常用的管理类,如全体房间管理器,人物管理器等)。
private void OnTriggerEnter2D(Collider2D collision)
{
if(isControllable && collision.transform.CompareTag("MoveDirection"))
{
if(collision.transform.name == "Door_Left")
{
GameManager.Instance.roomGenerator.MoveToNextRoom(Vector2.left);
}
if (collision.transform.name == "Door_Right")
{
GameManager.Instance.roomGenerator.MoveToNextRoom(Vector2.right);
}
if (collision.transform.name == "Door_Up")
{
GameManager.Instance.roomGenerator.MoveToNextRoom(Vector2.up);
}
if (collision.transform.name == "Door_Down")
{
GameManager.Instance.roomGenerator.MoveToNextRoom(Vector2.down);
}
}
}
由于素材原因,本来考虑的简单切换摄像机位置的方法不太适用(具体是给每个房间装上触发器,当触发器触发就传对应房间的transform),但是这个素材房间与房间之间并不是直通的,是从B点跳到A点,因此不仅要切换摄像头,也需要跳转人物的位置,用之前的方法需要人物走到黑色连接区域才能切换地图,很明显不合理,因此这里就可以用门的触发器来判断是否跨越房间(上面的代码)。
这里用了协程是因为理论上玩家切换房间时的那零点几秒内是不可以动的,然后处理房间内生成物等信息,目前处理内容不多,因此可以不用等待地图处理完后一帧再调用,但之后加上了房间载入信息的话可能就需要等待一帧。
public void MoveToNextRoom(Vector2 MoveDirection)
{
StartCoroutine(MoveToDesignativetRoom(MoveDirection));
}
private IEnumerator MoveToDesignativetRoom(Vector2 MoveDirection)
{
Camera mainCamera = GameManager.Instance.myCamera;
Transform player = GameManager.Instance.playerPrefab.transform;
if (MoveDirection == Vector2.right)
{
Vector3 originPos = mainCamera.transform.position;
Vector3 targetPos = mainCamera.transform.position;
targetPos.x = mainCamera.transform.position.x + xOffset;
mainCamera.transform.position = Vector3.Lerp(originPos, targetPos, 5f);
player.transform.position = new Vector3(player.position.x + playerXOffset, player.position.y, player.position.z);
}
else if (MoveDirection == Vector2.left)
{
Vector3 originPos = mainCamera.transform.position;
Vector3 targetPos = mainCamera.transform.position;
targetPos.x = mainCamera.transform.position.x - xOffset;
mainCamera.transform.position = Vector3.Lerp(originPos, targetPos, 5f);
player.position = new Vector3(player.position.x - playerXOffset, player.position.y, player.position.z);
}
else if (MoveDirection == Vector2.up)
{
Vector3 originPos = mainCamera.transform.position;
Vector3 targetPos = mainCamera.transform.position;
targetPos.y = mainCamera.transform.position.y + yOffset;
mainCamera.transform.position = Vector3.Lerp(originPos, targetPos, 5f);
player.position = new Vector3(player.position.x, player.position.y + playerYOffset, player.position.z);
}
else if (MoveDirection == Vector2.down)
{
Vector3 originPos = mainCamera.transform.position;
Vector3 targetPos = mainCamera.transform.position;
targetPos.y = mainCamera.transform.position.y - yOffset;
mainCamera.transform.position = Vector3.Lerp(originPos, targetPos, 5f);
player.position = new Vector3(player.position.x, player.position.y - playerYOffset, player.position.z);
}
yield return null;
}
最后是发射子弹的逻辑,通过对象池来优化内存,简而言之就是判断对象池中是否有子弹,如果有则直接使用对象池中的子弹,如果没有就生成一个子弹。
public class BulletPools : MonoBehaviour
{
public GameObject bulletPrefab;
Queue pool = new Queue();
public GameObject Take()
{
if (pool.Count > 0)
{
GameObject instanceToReuse = pool.Dequeue();
instanceToReuse.SetActive(true);
return instanceToReuse;
}
return Instantiate(bulletPrefab);
}
public void Back(GameObject gameObjectToPool)
{
pool.Enqueue(gameObjectToPool);
gameObjectToPool.transform.SetParent(transform);
gameObjectToPool.SetActive(false);
}
}
然后以撒的子弹是有重力因素影响的,因此生成之后可以把重力大小调大,然后在他消失的时候将重力清零。
public void Initialization()
{
isDestory = false;
damage = player.Damage;
playerKnockback = player.Knockback;
//子弹一段时间后触发自动销毁
Invoke("AutoDestroy", player.Range * 0.03f);
}
///
/// 自动销毁
///
void AutoDestroy()
{
if (isDestory) { return; }
//子弹快速下落一小段时间后销毁
myRigidbody.gravityScale = 1.7f;
Invoke("Destroy", 0.13f);
}
///
/// 销毁
///
void Destroy()
{
//关闭重力,停止移动,关闭碰撞体,播放消失动画,返回对象池
isDestory = true;
myRigidbody.gravityScale = 0;
myRigidbody.velocity = Vector2.zero;
myCollider.enabled = false;
animator.Play("Destory");
StartCoroutine(GoBackToPool());
}
///
/// 回收到对象池
///
///
IEnumerator GoBackToPool()
{
yield return WaitSeconds;
transform.position = Vector2.zero;
myCollider.enabled = true;
animator.Play("Idle");
animator.Update(0);
bulletPool.Back(gameObject);
}
最后在人物界面调用对象池中的Take()方法,通过玩家输入的不同方向对子弹施加一个力即可,这个可以通过自己需求和射程自行调整,目前子弹完成部分完成一半了,接下来就是根据碰撞到不同tag的物体进行分类处理。