赞
踩
详见https://blog.csdn.net/xiji333/article/details/109621328。
首先来修改之前代码中的一个问题,你可能已经发现了,就是当人物从高处落下(不是跳跃后落下)时,动画并没有切换到 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。
首先来制作老鹰:
然后制作老鹰的动画:
接下来开始写控制老鹰移动的代码,这里用的逻辑和青蛙不一样哦,我打算指定一个上下边界,让老鹰在这个边界内上下移动:
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(); …… } …… } } }
给
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(); } }
人物添加跳跃、受伤、拾取樱桃的音效并通过代码控制:
这一节我们将创建一个对话框,用来在恰当的时机提示用户如何进入下一个关卡,那么就要用到
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,第二帧背景的透明度回到之前设置的值,文字部分保持不变,第三帧文字部分的透明度回到之前设置的值。这时候点击播放就可以看到淡入效果啦。
在开始下一章的内容之前,你需要自己搭建第二关的地形:
首先给我们的场景底部加一条
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,在哪可以看到呢?
在开始之前,我们需要设置一下趴下的按键:
然后制作趴下的动画并设置状态机:
控制动画的代码很好写,不多赘述。但是当人物下蹲时,我们也应该修改它的碰撞体的位置和大小,或者使用一个新的碰撞体,更进一步,我们不能无条件的信任玩家,如果他们下蹲进入了一个障碍物,然后站立起来,此时我们依然认为人物回到了
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); } } } }
这一节我们来做一些简单的
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轴:
这一节用来优化之前的代码或者实现。首先可以清除 A n i m a t o r Animator Animator中一些没用的条件,比如人物中的那个 i d l e idle idle变量。视频中提到的其他问题我暂时没有遇到(因为我的人物只有 1 1 1个碰撞体),所以不打算修改了,至于移动和跳跃的手感问题我打算放到最后说。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。