文章目录
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
minSpeed、maxSpeed、jumpForce这三个数的值,不过对于我的地图而言,上面的这种做法够用了;其次是如何拿到落点的有效位置,我这里只能做近似估计,而且要记录青蛙初始的
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个碰撞体),所以不打算修改了,至于移动和跳跃的手感问题我打算放到最后说。