Unity Sunny Land开发流程(二)

0.开发流程一

  详见https://blog.csdn.net/xiji333/article/details/109621328

1.Animation Events

  首先来修改之前代码中的一个问题,你可能已经发现了,就是当人物从高处落下(不是跳跃后落下)时,动画并没有切换到 f a l l i n g falling falling,依然是 i d l e idle idle状态,而且此时落到敌人头上并不会消灭敌人,因为动画不是 f a l l i n g falling falling状态,现在来解决这个问题吧:

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

public class PlayerController : MonoBehaviour
{
    
    
    void SwitchAnimation()
    {
    
    
        //animator.SetBool("idle", false);
        if (rb2d.velocity.y < 0 && !boxCollider2D.IsTouchingLayers(ground)) //y轴速度<0且没有接触到地面时
        {
    
    
            animator.SetBool("falling", true);
        }
    }
}

  记得也要修改 p l a y e r player player的状态机。
  接下来制作敌人的动画:
在这里插入图片描述
  设置状态机:
在这里插入图片描述
  在修改代码之前,我们先来捋一下思路。我们要实现的是青蛙的移动,但是青蛙不能一直在跳跃呀,他需要适时的回到 i d l e idle idle状态,能不能找到一种方法,让青蛙在 i d l e idle idle动画播放完毕后自动跳跃一次呢?可以,通过 A n i m a t i o n   E v e n t s Animation\ Events Animation Events我们可以在某个动画的某一帧设置一个事件,让他调用某个函数。那么我们可以在 i d l e idle idle结束的时候设置一个 e v e n t s events events,调用函数使青蛙进入跳跃状态,同时我们还需要在 u p d a t e update update内修改播放的动画,而且青蛙移动的逻辑也需要修改了,如果青蛙在跳跃过程中转向的话肯定会非常奇怪吧?所以我,我们需要提前判断落点位置是否在地面上,如果不在的话就需要提前进行转向啦。

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

public class Enemy_Frog : MonoBehaviour
{
    
    
    private Rigidbody2D rb2d;
    private Animator animator;
    private CircleCollider2D circleCollider2D;

    public LayerMask ground;
    //为了增加游戏的多样性 我们可以设置minSpeed和maxSpeed 随机选择中间的某个值作为青蛙的移动速度
    public float minSpeed = 2.5f;
    public float maxSpeed = 4f;
    public float jumpForce = 4.5f;
    [SerializeField]
    private bool faceLeft = true;
    //记录初始时的y坐标
    private float initPositionY;
    // Start is called before the first frame update
    void Start()
    {
    
    
        rb2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        circleCollider2D = GetComponent<CircleCollider2D>();
        initPositionY = transform.position.y;
    }

    // Update is called once per frame
    void Update()
    {
    
    
        SwitchAnimation();
    }

    //这个函数将通过animation event调用!
    void Movement()
    {
    
    
        Vector2 frontPosition = transform.position;
        frontPosition.y = initPositionY;
        float speed = Random.Range(minSpeed, maxSpeed);
        //预测落地 这里并没有精确计算 不过和你的jumpForce有关系
        if (faceLeft)
            frontPosition += Vector2.left * speed;
        else
            frontPosition += Vector2.right * speed;
        if (!Physics2D.Linecast(frontPosition + Vector2.down, frontPosition, ground) || Physics2D.Linecast(frontPosition, frontPosition + Vector2.up * jumpForce, ground)) //没有检测到地面 或者头上有障碍物 
        {
    
    
            faceLeft = !faceLeft;
            transform.localScale = new Vector3(faceLeft ? 1 : -1, 1, 1); //角色反向
            //注意此时应该结束这个函数 不然frog反向后依然会跳出去 然而你并不能确定这次跳跃的有效性
            animator.Play("Frog_idle", 0, 0f);
            return;
        }
        rb2d.velocity = new Vector2(faceLeft ? -speed : speed, jumpForce);
        animator.SetBool("jumping", true);
    }

    void SwitchAnimation()
    {
    
    
        if (animator.GetBool("jumping")) //跳跃状态
        {
    
    
            if (rb2d.velocity.y < 0)
            {
    
    
                animator.SetBool("jumping", false);
                animator.SetBool("falling", true);
            }
        }
        else if (animator.GetBool("falling")) //下落状态
        {
    
    
            if (circleCollider2D.IsTouchingLayers(ground))
            {
    
    
                animator.SetBool("falling", false);
            }
        }
    }

    private void OnDrawGizmosSelected()
    {
    
    
        Vector2 frontPosition = new Vector2(transform.position.x, initPositionY) + Vector2.left * minSpeed * (faceLeft ? 1 : -1);
        Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up * jumpForce);
        frontPosition = new Vector2(transform.position.x, initPositionY) + Vector2.left * maxSpeed * (faceLeft ? 1 : -1);
        Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up * jumpForce);
    }

}

在这里插入图片描述
  先扯一下 d e b u g debug debug的辛酸历程,首先是青蛙的移动问题,代码中只判断了落点的有效性,然而这并不能证明当前位置和落点之间都是地面,所以青蛙还是有几率会跳出地图外,这取决于你地图的搭建以及 m i n S p e e d 、 m a x S p e e d 、 j u m p F o r c e minSpeed、maxSpeed、jumpForce minSpeedmaxSpeedjumpForce这三个数的值,不过对于我的地图而言,上面的这种做法够用了;其次是如何拿到落点的有效位置,我这里只能做近似估计,而且要记录青蛙初始的 y y y坐标,否则基于当前坐标再判断落点的话是比较麻烦的;最后是一种比较尴尬的情况,因为 s p e e d speed speed是随机的,那么有可能出现青蛙往左跳往右跳都不行的情况,我的做法是让青蛙原地不动,再次播放 i d l e idle idle动画,顺便提一下,我的动画的 l o o p loop loop选项被关闭了,所以通过代码再次播放,如果你的动画是重复播放的,那么就无需通过代码控制。综上,这种做法并不完美,但是我目前并不打算修改orz。

2.Class调用&死亡动画&继承多态

  首先来制作老鹰:
在这里插入图片描述
  然后制作老鹰的动画:
在这里插入图片描述
  接下来开始写控制老鹰移动的代码,这里用的逻辑和青蛙不一样哦,我打算指定一个上下边界,让老鹰在这个边界内上下移动:

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

public class Enemy_Eagle : MonoBehaviour
{
    
    
    private Rigidbody2D rb2d;

    private float minPositionY, maxPositionY;
    [SerializeField]
    private bool isUp = false;

    public float moveLength = 2.5f;
    public float speed = 3.5f;
    // Start is called before the first frame update
    void Start()
    {
    
    
        rb2d = GetComponent<Rigidbody2D>();
        minPositionY = transform.position.y - moveLength;
        maxPositionY = transform.position.y + moveLength;
    }

    // Update is called once per frame
    void Update()
    {
    
    
        Movement();
    }

    void Movement()
    {
    
    
        if (transform.position.y > maxPositionY)
            isUp = false;
        else if (transform.position.y < minPositionY)
            isUp = true;
        rb2d.velocity = new Vector2(0, isUp ? speed : -speed);
    }

    private void OnDrawGizmosSelected()
    {
    
    
        Gizmos.DrawLine(new Vector2(transform.position.x, minPositionY), new Vector2(transform.position.x, maxPositionY));
    }
}

  然后制作消灭青蛙的动画:
在这里插入图片描述
在这里插入图片描述

  现在来捋一下思路,我们之前是怎么消灭青蛙的?是在人物的 O n C o l l i s i o n E n t e r 2 D OnCollisionEnter2D OnCollisionEnter2D里面直接销毁了对应的游戏对象。当游戏比较简单时这么做无可厚非,但是现在青蛙死亡要播放一段动画,如果把这段逻辑也交给人物来控制的话,是不是不太合理?我们希望青蛙自己控制这段逻辑,并向外提供一个接口供人调用即可。 o k ok ok,那么还有一个问题,青蛙要先播放完动画再消灭自己,这个怎么控制呢?当然是利用我们上一节刚学过的 e v e n t s events events辣:
E n e m y F r o g . c s : Enemy_Frog.cs: EnemyFrog.cs

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

public class Enemy_Frog : MonoBehaviour
{
    
    
    private void Death()
    {
    
    
        Destroy(gameObject);
    }

    //供player调用
    public void JumpOn()
    {
    
    
        animator.SetTrigger("death");
    }
}

在这里插入图片描述
P l a y e r C o n t r o l l e r . c s : PlayerController.cs: PlayerController.cs

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

public class PlayerController : MonoBehaviour
{
    
    
	……
    private void OnCollisionEnter2D(Collision2D collision)
    {
    
    
        if (collision.gameObject.tag == "Enemy")
        {
    
    
            if (animator.GetBool("falling"))    //掉落状态
            {
    
    
                collision.gameObject.GetComponent<Enemy_Frog>().JumpOn();
				……
            }
            ……
        }
    }
}

  至此青蛙的死亡动画已经做好了,但是先别激动,你还有老鹰的没做23333。不过在动手之前,我想请你认真思考一下,如果再从头实现老鹰的死亡动画的话,是不是大部分代码都是重复的?如果以后还要增加新的敌人,那岂不是每个都要重写,而且在人物的代码中还要具体区分每一个敌人。想想就头痛,那有没有更好的办法呢?当然有,我们可以使用面向对象的思维,给所有的敌人写一个基类,让敌人继承这个基类。这样不仅可以提高代码的复用,还可以让程序更加灵活。接下来将涉及到继承多态等知识,大家可以先学习一波。
E n e m y . c s : Enemy.cs: Enemy.cs

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

public class Enemy : MonoBehaviour
{
    
    
    // Start is called before the first frame update
    protected Animator animator;
    protected virtual void Start()
    {
    
    
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
    
    
        
    }
    private void Death()
    {
    
    
        Destroy(gameObject);
    }

    //供player调用
    public void JumpOn()
    {
    
    
        animator.SetTrigger("death");
    }
}

  老鹰和青蛙需要继承这个基类,同时它们不需要自己定义 a n i m a t o r animator animator变量了, D e a t h Death Death J u m p O n JumpOn JumpOn两个函数也可以省去。

E n e m y F r o g . c s : Enemy_Frog.cs: EnemyFrog.cs

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

public class Enemy_Frog : Enemy
{
    
    

    protected override void Start()
    {
    
    
        //调用基类的start
        base.Start();
    }

P l a y e r C o n t r o l l e r . c s : PlayerController.cs: PlayerController.cs

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

public class PlayerController : MonoBehaviour
{
    
    
    ……
    private void OnCollisionEnter2D(Collision2D collision)
    {
    
    
        if (collision.gameObject.tag == "Enemy")
        {
    
    
            if (animator.GetBool("falling"))    //掉落状态
            {
    
    
                //注意这里得到的组件是 所有敌人的基类 Enemy
                collision.gameObject.GetComponent<Enemy>().JumpOn();
                ……
            }
            ……
        }
    }
}

3.音效Audio

在这里插入图片描述
  给 P l a y e r Player Player添加背景音乐:
在这里插入图片描述
  给两个敌人添加爆炸的音效并修改代码:

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

public class Enemy : MonoBehaviour
{
    
    
    // Start is called before the first frame update
    protected Animator animator;
    protected AudioSource audioSource;
    protected virtual void Start()
    {
    
    
        animator = GetComponent<Animator>();
        audioSource = GetComponent<AudioSource>();
    }

    // Update is called once per frame
    void Update()
    {
    
    
        
    }
    private void Death()
    {
    
    
        Destroy(gameObject);
    }

    //供player调用
    public void JumpOn()
    {
    
    
        animator.SetTrigger("death");
        audioSource.Play();
    }
}

  人物添加跳跃、受伤、拾取樱桃的音效并通过代码控制:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.对话框Dialog&淡入动画效果

  这一节我们将创建一个对话框,用来在恰当的时机提示用户如何进入下一个关卡,那么就要用到 U I UI UI了:
在这里插入图片描述
  接下来设置它的位置,注意要修改锚点:
在这里插入图片描述
  接下来为它添加一个子物体 T e x t Text Text,并设置相关的属性:
在这里插入图片描述
  我想把房子的门当作下一关的入口,所以我需要一个碰撞体,并把它当作触发器使用,同时创建一个脚本用来控制对话框的显示与关闭。如果你需要多个这样的入口,那么你可以自己实现一个预制体——带有控制脚本和碰撞体的空游戏对象,这样可以不用重新写代码,只需要把它放置在合适的位置即可。
在这里插入图片描述

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

public class EnterDialog : MonoBehaviour
{
    
    
    // Start is called before the first frame update
    public GameObject dialog;
    void Start()
    {
    
    
        
    }

    // Update is called once per frame
    void Update()
    {
    
    
        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
    
    
        if (collision.tag == "Player")
        {
    
    
            dialog.SetActive(true);
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
    
    
        if (collision.tag == "Player")
        {
    
    
            dialog.SetActive(false);
        }
    }
}

  接下来给我们的对话框制作一段简单的动画效果吧~注意这次的动画和之前的不太一样,之前是通过多张图片来制作的,这次要录制动画。点击下图所示的红色圆圈,就可以开始录制了。
在这里插入图片描述
  这个时候你可以修改对应物体(动画所对应的物体)的颜色、透明度、位置、旋转等等属性,这些修改会以关键帧的形式表现出来:
在这里插入图片描述
  以我制作的为例,第一帧对话框的透明度为 0 0 0,第二帧背景的透明度回到之前设置的值,文字部分保持不变,第三帧文字部分的透明度回到之前设置的值。这时候点击播放就可以看到淡入效果啦。
  在开始下一章的内容之前,你需要自己搭建第二关的地形:
在这里插入图片描述

5.场景控制SceneManager&Invoke&BuildIndex

  首先给我们的场景底部加一条 D e a d   L i n e Dead\ Line Dead Line,当玩家掉落到这条线以下时认为游戏失败,并重新加载第一个场景,我们希望这个加载可以滞后两秒执行( I n v o k e Invoke Invoke),同时停止播放背景音乐:
在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
    
    
    private void OnTriggerEnter2D(Collider2D collision)
    {
    
    
        if (collision.tag == "Collection")
        {
    
    
            pickCherryAudio.Play();
            Destroy(collision.gameObject);
            ++cherry;
            cherryText.text = cherry.ToString();
        }
        else if (collision.tag == "DeadLine")
        {
    
    
            //注意这个只会停止播放第一个
            GetComponent<AudioSource>().Stop();
            //延迟2s后执行
            Invoke(nameof(Restart), 2f);
        }
    }

    void Restart()
    {
    
    
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }

  接下来制作第一关到第二关的跳转。还记得我们之前制作的 d i a l o g dialog dialog吗,它是用来提示玩家进入下一关卡的,那么我们可以写一个脚本并将其挂在 d i a l o g dialog dialog上,这样只有当 d i a l o g dialog dialog为活跃状态时才能响应玩家的按键:

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

public class EnterHouse : MonoBehaviour
{
    
    
    // Update is called once per frame
    void Update()
    {
    
    
        if (Input.GetKeyDown(KeyCode.E))
        {
    
    
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
        }
    }
}

  上面函数的参数是场景的 b u i l d I n d e x buildIndex buildIndex,在哪可以看到呢?
在这里插入图片描述
在这里插入图片描述

6.趴下效果Crouch&Input设置

  在开始之前,我们需要设置一下趴下的按键:
在这里插入图片描述
在这里插入图片描述
  然后制作趴下的动画并设置状态机:
在这里插入图片描述
  控制动画的代码很好写,不多赘述。但是当人物下蹲时,我们也应该修改它的碰撞体的位置和大小,或者使用一个新的碰撞体,更进一步,我们不能无条件的信任玩家,如果他们下蹲进入了一个障碍物,然后站立起来,此时我们依然认为人物回到了 i d l e idle idle状态的话就会有问题,比如卡在障碍物里面、播放错误的动画等等。所以我们需要判断玩家的头上有没有障碍物。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
    
    
    private Rigidbody2D rb2d;
    private Animator animator;
    private BoxCollider2D boxCollider2D;
    [SerializeField]
    private int cherry = 0;
    [SerializeField]
    private bool isHurt = false;
    private Vector2 initBoxCollider2DOffset;
    private Vector2 initBoxCollider2DSize;
    private Vector2 crouchBoxCollider2DOffset = new Vector2(0, -0.57f);
    private Vector2 crouchBoxCollider2DSize = new Vector2(0.92f, 0.85f);


    public Text cherryText;
    public float speed = 7f;
    public float jumpForce = 7f;
    public LayerMask ground;
    public AudioSource jumpAudio;
    public AudioSource hurtAudio;
    public AudioSource pickCherryAudio;
    // Start is called before the first frame update
    void Start()
    {
    
    
        rb2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        boxCollider2D = GetComponent<BoxCollider2D>();
        initBoxCollider2DOffset = boxCollider2D.offset;
        initBoxCollider2DSize = boxCollider2D.size;
    }

    // Update is called once per frame
    void Update()
    {
    
    
        if (!isHurt)//非受伤状态
        {
    
    
            Movement();
        }
        SwitchAnimation();
    }
  
    void Movement()
    {
    
    
        float horizontalMove = Input.GetAxis("Horizontal");
        int faceDirection = (int)Input.GetAxisRaw("Horizontal");
        rb2d.velocity = new Vector2(horizontalMove * speed, rb2d.velocity.y);
        animator.SetFloat("running", Mathf.Abs(horizontalMove));
        if (faceDirection != 0)
        {
    
    
            transform.localScale = new Vector3(faceDirection, 1, 1);
        }
        if (Input.GetButtonDown("Jump") && boxCollider2D.IsTouchingLayers(ground))
        {
    
    
            rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);
            animator.SetBool("jumping", true);
            jumpAudio.Play();
        }
        Crouch();
    }

    void Crouch()
    {
    
    
        //头顶上没有障碍物
        if (!Physics2D.OverlapCircle(transform.position, 0.4f, ground))
        {
    
    
            //持续按下
            if (Input.GetButton("Crouch"))
            {
    
    
                animator.SetBool("crouching", true);
                boxCollider2D.offset = crouchBoxCollider2DOffset;
                boxCollider2D.size = crouchBoxCollider2DSize;
            }
            else
            {
    
    
                animator.SetBool("crouching", false);
                boxCollider2D.offset = initBoxCollider2DOffset;
                boxCollider2D.size = initBoxCollider2DSize;
            }
        }
    }

    void SwitchAnimation()
    {
    
    
        //animator.SetBool("idle", false);
        if (rb2d.velocity.y < 0 && !boxCollider2D.IsTouchingLayers(ground)) //y轴速度<0且没有接触到地面时
        {
    
    
            animator.SetBool("falling", true);
        }
        if (animator.GetBool("jumping")) //跳跃状态
        {
    
    
            if (rb2d.velocity.y < 0)
            {
    
    
                animator.SetBool("jumping", false);
                animator.SetBool("falling", true);
            }
        }
        else if (animator.GetBool("falling")) //下落状态
        {
    
    
            if (boxCollider2D.IsTouchingLayers(ground))
            {
    
    
                animator.SetBool("falling", false);
                animator.SetBool("idle", true);
            }
        }
        else if (isHurt) //受伤状态
        {
    
    
            animator.SetBool("hurt", true);
            int sign = rb2d.velocity.x < 0 ? -1 : 1;
            rb2d.velocity += new Vector2(speed * Time.deltaTime, 0f) * -sign;
            if (Mathf.Abs(rb2d.velocity.x) < 0.1f)
            {
    
    
                isHurt = false;
                animator.SetBool("hurt", false);
            }
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
    
    
        if (collision.tag == "Collection")
        {
    
    
            pickCherryAudio.Play();
            Destroy(collision.gameObject);
            ++cherry;
            cherryText.text = cherry.ToString();
        }
        else if (collision.tag == "DeadLine")
        {
    
    
            //注意这个只会停止播放第一个
            GetComponent<AudioSource>().Stop();
            //延迟2s后执行
            Invoke(nameof(Restart), 2f);
        }
    }

    void Restart()
    {
    
    
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
    
    
        if (collision.gameObject.tag == "Enemy")
        {
    
    
            if (animator.GetBool("falling"))    //掉落状态
            {
    
    
                //注意这里得到的组件是 所有敌人的基类 Enemy
                collision.gameObject.GetComponent<Enemy>().JumpOn();
                rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);    //小跳效果
                animator.SetBool("jumping", true);
            }
            else if (transform.position.x < collision.gameObject.transform.position.x)//左侧
            {
    
    
                hurtAudio.Play();
                isHurt = true;
                rb2d.velocity = new Vector2(-7f, rb2d.velocity.y);
            }
            else if(transform.position.x > collision.gameObject.transform.position.x)//右侧
            {
    
    
                hurtAudio.Play();
                isHurt = true;
                rb2d.velocity = new Vector2(7f, rb2d.velocity.y);
            }
        }
    }

}

7.2D光效

  这一节我们来做一些简单的 2 D 2D 2D光效。我希望第二个场景总体是暗的,只有壁火和人物身上有光源。首先修改 t i l e m a p tilemap tilemap的渲染材质:
在这里插入图片描述
  自己创建一个 m a t e r i a l material material,把它添加到人物和门上面:
在这里插入图片描述
  然后在合适的位置添加点光源吧!记得修改光源的 z z z轴:
在这里插入图片描述

8.优化代码Fix code

  这一节用来优化之前的代码或者实现。首先可以清除 A n i m a t o r Animator Animator中一些没用的条件,比如人物中的那个 i d l e idle idle变量。视频中提到的其他问题我暂时没有遇到(因为我的人物只有 1 1 1个碰撞体),所以不打算修改了,至于移动和跳跃的手感问题我打算放到最后说。

猜你喜欢

转载自blog.csdn.net/xiji333/article/details/109955327