Tizeng's blog Ordinary Gamer

Demo笔记1——平台跳跃

2019-08-06
Tizeng

Tilemap

用这个工具可以极大提高关卡制作效率,在不同的tilemap上可以设置不同的像素图,也可以为障碍物的tilemap加上Tilemap Collider2D来自动生成collider。

横版游戏角色的坑

横版游戏的角色与俯视视角的略有不同,首先只需要左右两个方向的移动,和往上方跳跃的操作,因此只需要接收“Horizontal”方向的变化。

在实现跳跃时不能简单的用Addforce,要先将RigidBody的y方向速度置为0,否则会与重力抵消一部分。

俯视视角时,移动方向是四周,因此用一个moveDirection的二维向量就可以表示出当前移动的方向,然后乘上速度,而横版游戏中这就会出现bug,因为我们不能再y轴增加任何速度,否则就相当于改变了重力的影响。下面是两个其他常见的bug:

  • 跳跃后碰到其他地面会弹起

  • 斜坡往下滑时一直播放Falling动画

这两个都是因为在写跳跃逻辑时出错或不准确导致。

我们把跳跃分为三个阶段:grounded、jumping、falling,在地面上时如果有跳跃输入,则进入jumping状态,此时角色在空中,且具有向上的速度,由重力影响不断减速,速度减为0后开始下落,进入falling状态,直到再次接触地面。不同的状态分别播放不同的动画,如果发现角色在某个情况(比如斜坡)一直播放下落动画,就说明其一直处于下落状态,这可能是角色脚下的判别器没有检测到地面,如下图所示:

ground check

为了防止这种情况,我们把ground check加到两个,一边一个同时检测,就不会出现这种情况了。

行为逻辑

描述:场景中有NPC,NPC会有一些行为,主角靠近之后可以对其附身,表现为主角的输入开始作用于该NPC,镜头也随之跟随,在特定情况(如该NPC死亡,或角色主动)下玩家回到操控主角的状态,镜头也随之切换。

处理输入

要实现这个功能,就要将主角和NPC的行为逻辑和输入逻辑分开,输入脚本只接收输入,不做任何操作,然后将输入传给行为脚本,行为脚本根据传过来的输入开始不同的行为,这样我们在需要附身时,只需要将输入脚本的输出从主角身上转移到对应的NPC身上,就可以实现操作的转移,而NPC没有被附身的时候,也可以用脚本生成简单的输入作为NPC自己的行为(AI),下面看具体代码:

using UnityEngine;

public class PlayerInput : MonoBehaviour {
    Controller controller;
    Vector2 moveDirection;
    bool isPressingSpace = false;

    void Start() {
        controller = GetComponent<Controller>();
    }

    void Update() {
        if(controller == null){
            return;
        }
        processInput();
        passInput();
    }

    public Controller getController() {
        return controller;
    }

    void processInput() {
        moveDirection.x = Input.GetAxisRaw("Horizontal"); // move
        //moveDirection.y = Input.GetAxisRaw("Vertical");
        if (Input.GetButtonDown("Jump")) { // jump
            isPressingSpace = true;
        }
    }

    void passInput() {
        controller.input(moveDirection.x, moveDirection.y, isPressingSpace);
        isPressingSpace = false;
    }
}

角色行为

为了复用部分代码,为角色的行为脚本定义一个基类Controller,后面不同的角色和NPC都继承这个基类,Input脚本在获取的时候只需要GetComponent<Controller>就行了:

using UnityEngine;

public class Controller : MonoBehaviour {
    [Range(1f, 20f)]
    public float speed = 10f;

    [HideInInspector]
    public Vector2 moveDirection;

    protected Rigidbody2D rb;
    protected bool facingRight = true;
    protected bool isPressingSpace = false;

    public void input(float x, float y, bool _isPressingSpace) {
        moveDirection.x = x;
        moveDirection.y = y;
        isPressingSpace = _isPressingSpace;
    }

    public void resetInput() {
        moveDirection = Vector2.zero;
        isPressingSpace = false;
    }
}

然后是主角的行为脚本,左右移动,落地时可以跳,不能二段跳:

using System;
using UnityEngine;

public class PlayerController : Controller {
    [Range(1f, 30f)]
    public float jumpForce = 10f;

    public Animator animator;
    public Transform groundLeftCheck;
    public Transform groundRightCheck;
    public LayerMask ground;

    //private Rigidbody2D rb;
    //private Vector2 moveDirection;
    private float fallingSpeed = -7f;
    //private bool facingRight = true;
    private bool isGrounded = true;
    //private bool canDoubleJump = true;
    private bool isJumping = false;
    private bool isFalling = false;
    //private bool isJumping = false;

    private void Start() {
        rb = GetComponent<Rigidbody2D>();
    }

    private void FixedUpdate() {
        isGrounded = Physics2D.Linecast(transform.position, groundLeftCheck.position, ground) ||
                    Physics2D.Linecast(transform.position, groundRightCheck.position, ground);
    }

    private void Update() {
        //processInput();
        move();
        animate();
        //Debug.Log(moveDirection);
    }

    void processInput() {
        moveDirection.x = Input.GetAxisRaw("Horizontal"); // move
        moveDirection.y = rb.velocity.y; // gravity
        if (Input.GetButtonDown("Jump")) { // jump
            isJumping = true; // 进入跳跃状态
        }
    }

    public void move() {
        if (isPressingSpace) {
            isJumping = true;
        }
        moveDirection.x *= speed;
        moveDirection.y = rb.velocity.y; // gravity

        if ((rb.velocity.y < 1f && rb.velocity.y > -1f) && !isGrounded) { // 上升到最高点,纵向速度为0,跳跃状态结束(速度变化过快时失效
            isJumping = false;
        }
        if (moveDirection.y < -1f && !isGrounded) { // 速度为负,且不着地,进入下落状态
            isFalling = true;
        }

        if (isJumping && isGrounded) { // 在地上,起跳
            moveDirection.y = jumpForce;
        }
        //else if (canDoubleJump) {
        //    moveDirection.y = 15f;
        //    canDoubleJump = false;
        //}
        else if (moveDirection.y > 0.1f) { // 已起跳,加重力
            moveDirection.y = rb.velocity.y;
        }
        else if (isFalling) { // 开始下落,固定下落速度
            moveDirection.y = fallingSpeed;
        }
        rb.velocity = moveDirection; // 最后给刚体速度
    }

    public void animate() {
        animator.SetFloat("Horizontal", moveDirection.x);
        animator.SetBool("isFalling", isFalling);
        animator.SetBool("isJumping", isJumping);
        animator.SetBool("isGrounded", isGrounded);
        flip(moveDirection.x);
    }

    void flip(float horizontal) {
        if ((horizontal > 0 && facingRight == false) || (horizontal < 0 && facingRight == true)) {
            facingRight = !facingRight;
            transform.Rotate(0, 180f, 0);
        }
    }

    private void OnCollisionEnter2D(Collision2D collision) {
        if(collision.gameObject.tag == "Ground") {
            //canDoubleJump = true;
            isGrounded = true;
            isFalling = false;
            isJumping = false;
            animator.SetBool("isFalling", isFalling);
            animator.SetBool("isJumping", isJumping);
            animator.SetBool("isGrounded", isGrounded);
        }
    }
}

最后是一个NPC小鸟的行为脚本,小鸟的逻辑类似flappy bird,每按一下往上飞一点,没有输入就会下落,逻辑上其实就是可以无限跳跃,然后控制跳跃的力度:

using UnityEngine;

public class BirdController : Controller {
    [Range(1f, 20f)]
    public float jumpForce = 5f;

    Animator animator;

    void Start() {
        rb = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        moveDirection = Vector2.zero;
    }

    void Update() {
        move();
        animate();
    }

    void move() {
        moveDirection.x *= speed;
        moveDirection.y = rb.velocity.y;
        if (isPressingSpace) {
            moveDirection.y = jumpForce;
        }
        rb.velocity = moveDirection;
    }

    void animate() {
        flip(moveDirection.x);
    }

    void flip(float horizontal) {
        if ((horizontal > 0 && facingRight == false) || (horizontal < 0 && facingRight == true)) {
            facingRight = !facingRight;
            transform.Rotate(0, 180f, 0);
        }
    }
}

在主角没有附身小鸟的时候,小鸟需要有一些行为,这里简单起见,就让它不停的原地上下,为了实现这个效果需要调整每次自动跳跃的间隔,太大太小都不行,这里根据我的调整间隔应为0.76s:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AI : MonoBehaviour {
    public float inputFrequency = 0.76f;

    Controller controller;
    bool isPressingSpace = false;

    void Start() {
        controller = GetComponent<Controller>();
        StartCoroutine(birdMove());
    }

    private void Update() {
        controller.input(0, 0, isPressingSpace);
        isPressingSpace = false;
    }

    IEnumerator birdMove() {
        while (true) {
            isPressingSpace = true;
            yield return new WaitForSeconds(inputFrequency);
        }
    }
}

附身技能

为了较低模块的耦合性,我将角色的附身技能这块独立出来,作为一个单独的脚本控制:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerSkill : MonoBehaviour {
    public new Camera camera;

    bool skillHeck;
    GameObject target;

    private void Start() {
        StartCoroutine(resetHeck());
    }

    private void Update() {
        //Debug.Log("is hecking = " + skillHeck);
    }

    IEnumerator resetHeck() {
        while (true) {
            skillHeck = false;
            yield return new WaitForSeconds(.1f);
        }
    }

    public void pressHeck() {
        skillHeck = true;
    }

    public void releaseHeck() {
        skillHeck = false;
    }

    public void heck() {
        if (GetComponent<PlayerInput>() == null || target.GetComponent<PlayerInput>() == null) {
            Debug.Log("PlayerInput does not exist");
            return;
        }
        GetComponent<PlayerInput>().getController().input(0, 0, false); // 重置输入,不然出错
        GetComponent<PlayerInput>().enabled = false;
        if(target.GetComponent<AI>() != null) {
            target.GetComponent<AI>().enabled = false;
        }
        target.GetComponent<PlayerInput>().enabled = true;

        // 对准摄像头
        if (camera == null) {
            Debug.LogError("No cam");
            return;
        }
        camera.GetComponent<CameraFollower>().setTarget(target);
    }

    public void retriveControl() {
        if(target == null) {
            Debug.LogError("No target");
            return;
        }
        target.GetComponent<PlayerInput>().enabled = false; // 外部:取消用户输入
        if (target.GetComponent<AI>() != null) { // 开启AI输入
            target.GetComponent<AI>().enabled = true;
        }
        GetComponent<PlayerInput>().enabled = true; // 主角:恢复用户输入
        target.GetComponent<Controller>().resetInput(); // 重置目标的输入
        target = null;
        camera.GetComponent<CameraFollower>().setTarget(gameObject);
    }

    private void OnTriggerStay2D(Collider2D collision) {
        if (collision.gameObject.tag == "Bird" && skillHeck) {
            target = collision.gameObject;
            heck();
            Debug.Log("Hecking");
        }
    }
}

至于摄像头对准这块,现在是直接把位置设过去,略显突兀,后面还需要优化。


下一篇 Lua入门

Comments

Content