14-创建场景:小河
你好,我是悦创。
接下来,我们来制作我们的小河。
也就是,小河上面漂浮的木板,其实我们制作好这个公路和汽车之后呢,这个河流和木板基本上是一样的概念。
不过,在这里唯一的难点就是:小青蛙,如何跳到木板的时候,能跟着木板移动。另外还有一个难点就是,怎样判断:小青蛙是跳进河里了。
1. 搭建小河
首先,我们要搭建的是我们小河的部分。
在我们之前,其实也创建了我们的小河部分,我们继续使用同样的方法,来实现。
我们可以暂时把和放在和公路一起。
2. 创建空的父级物体
- 先选中两个角色
- 鼠标右键
- Create Empty Parent
- 取名字:River
3. 创建自动生成点
和我们的小汽车类似,我们也需要 SpawnPoint A、SpawnPoint B,表示从左向右移动的两个木板。
我们把空的生成点,创建出来,我们创建空的 GameObject 即可。
我们和之前类似,可以设置颜色。
复制一个 B 出来
并设置为蓝色,移动到另外一侧
在上面,我们暂时调整了一个位置,因为这不是最终的位置。我们还要添加木板看距离是不是可以的。
接下来,我们尝试创建一个木板看看。
4. 创建木板
拖到 SpawnPoint A 下,试一试:
rest Wood01 这样木板的坐标就会和他的父级同一个坐标。
接下来,我们可以拖拽木板的,到中间。看是否符合我们青蛙🐸跳跃之后,能不能正常跳跃到木板。「控制距离」
我们接下来拖拽第二块木板,到我们的生成点二的位置。
先拖拽到中间。
如果,我们觉得它们间隔太远,我们也可以调整它们的距离。以便青蛙真的能跳到。
5. 设置木板预制体
在我们之前的内容中,我们把小汽车生成点做完之后,我们做成了预制体。并且实现随机生成小汽车。
所以,我们的木板也是一样的。
所以,我们在生成木板的时候,也是生成三种木板。
木板和其他角色一样,也需要一些组件。接下来,我们一起添加。
修改为:Kinematic
我们把 Y 轴的值锁定一下,因为我们只需要木板横向移动。同时也不需要旋转,所以我们也把 Z 锁定🔒。
修改碰撞检测:
6. 添加碰撞体💥
我们需要把木板的阴影去掉就行,保留木板的范围就可以。
勾选 Is Trigger
7. 设置标签🏷️
另外也需要标签来设置,它「木板」碰撞的是什么。
我们单独为木板设置一个专属标签,因为木板是一个求生工具,所以需要专门的标签。
拖进预制体的文件夹中,先新建文件夹 Woods:
拖进去之后,重置预制体的坐标。
一个做好了,我们接下来要把剩下两个也要做好。
8. 设置剩下的木板预制体
设置碰撞体部分:
接下来,可以先删除前面的三个木板。
9. 绑定代码
10. 绑定物体
11. 测试运行
我们可以看见,报错了。
我们要学会 Debug 的方法,我们要学会看是哪一行出现了问题。
我们可以看到是 Spawner.cs:40
的问题。
private void Spawn()
{
// 在这个函数中,我们要学会如何生成物体
// 我们先试一试生成列表中的第一个
// Instabtiate(角色, 生成的位置,是否旋转,生成之后成为当前物体的子物体)
// transform.position 生成到当前角色的 position,因为我们的代码是绑定在 SpawnPoint 生成点上
// 不需要任何旋转的话,我们使用:Quaternion.identity,意思是保留当前的状态,不使用任何的旋转
// spawnObjects[0] 找到序号为 0 的角色
var index = Random.Range(0, spawnObjects.Count); // 浮点数左闭右闭,整数:左闭右开 , 使用我们 list 列表的个数 spawnObjects.Count
// Instantiate(spawnObjects[0], transform.position, Quaternion.identity, transform); // 放到 Start 中执行一下
// Instantiate(spawnObjects[index], transform.position, Quaternion.identity, transform); // 放到 Start 中执行一下
// 接下来,我们需要实现一个全新的代码,在 Spawner 调用 MoveForward 的代码
// 创建 car 变量,来存储创建的小汽车
// var car = Instantiate(spawnObjects[index], transform.position, Quaternion.identity, transform); // 放到 Start 中执行一下
// 和下面等价
GameObject car = Instantiate(spawnObjects[index], transform.position, Quaternion.identity, transform);
// 我们要取到代码,修改值
car.GetComponent<MoveForward>().dir = direction; // 修改为当前的 direction
}
其实我们可以看见,我们创建的是小汽车,所以我们可以修改一下。
我们没有把我们的木板进行绑定。
12. 为预制体绑定代码
三个木板相同的速度
接下来就是测试运行,并调整河水的距离、木板的距离。
一直调整,并调整距离,让青蛙可以轻松一只短跳到两个木板上。如果发现木板高了还是低了,那我们要多调整一下木板的间距。
——可以直接调整生成点的位置。这个过程需要不停的调整,才可以。
13. 问题
在上面我们成功实现了跳到木板,所以现在我们要判断,它什么时候碰到水面呢?又什么时候碰到木板。所以碰木板的话,我们刚刚给了木板标签,所以我们只要判断标签是木板就可以了。
那么水呢?我们也给水设置一个标签,就设为 Water。
13.1 设置水的标签
13.2 添加组件
添加组件实现是否有碰撞。
13.3 编写代码
找到我们的 Frog,中的 PlayerController.cs 进行编写。
private void OnTriggerStay2D(Collider2D other) // other:帮助我们更好理解,除了当前的青蛙以外,对方的 Collider2D,对方的碰撞器就是 other。
// 我们来判断
{
if (other.CompareTag("Water") && !isJump) // 如果青蛙是跳在水上了,并且没有在跳跃的情况下,其实就是游戏已经结束了
{
// 但是有可能是青蛙踩在了木板上,木板在水的上面。
// 所以,这个时候怎么办呢?
// 有一种极端的情况,就是青蛙跳到木板上,踩在木板的边缘,可能跟水有交接界,所以这个时候我们不容易判断。
}
// ---snip---
}
我们觉得,下图这样,小青蛙也不算游戏结束。
但是如果超过一半在水面,青蛙游戏就结束:
所以,我们会以青蛙的锚点:
如果这个锚点在水里的话,一定触发 Trigger。
但是,我们希望不仅仅判断是否在水里,我们还需要判断是否在木板上而且在木板上面的位置。
所以:在触发两个 Trigger 的时候,我们青蛙如果碰到木板,那么青蛙就跟木板走。
如果青蛙的代码同时监测到触发 Water 和木板的碰撞体,那么青蛙就跟着木板走。青蛙的点我们设置在中间下面。
所以,我们要判断所有的碰撞结果,我们应该怎么做呢?
我们使用 Unity 内置的物理组件。
// 判断碰撞检测返回的物体
private RaycastHit2D[] result; // 这是一个数组,使用数组需要初始化
所以,我们可以一开始就就进行初始化:
// 判断碰撞检测返回的物体
// private RaycastHit2D[] result; // 这是一个数组,使用数组需要初始化
// 所以,我们可以一开始就就进行初始化:
private RaycastHit2D[] result = new RaycastHit2D[2]; // 这是一个数组,使用数组需要初始化, 2是数组大小
private void OnTriggerStay2D(Collider2D other) // other:帮助我们更好理解,除了当前的青蛙以外,对方的 Collider2D,对方的碰撞器就是 other。
// 我们来判断
{
if (other.CompareTag("Water") && !isJump) // 如果青蛙是跳在水上了,并且没有在跳跃的情况下,其实就是游戏已经结束了
{
Physics2D.RaycastNonAlloc(transform.position + Vector3.up * 0.1f, Vector2.zero, result); // 原点
// 循环出我们的标签
// foreach () // foreach 就是可以不用序号来实现循环,范围是什么,就是我们 result
foreach(var hit in result) {
// 通过碰撞体返回标签
Debug.Log(hit.collider.tag); // 这样我们就可以实时的看见碰撞到的物体
}
}
// ---snip---
}
上图我们可以知道,碰撞体都实现判断了。判断,如果标签有 Wood ,就输出:在木板上。
private void OnTriggerStay2D(Collider2D other) // other:帮助我们更好理解,除了当前的青蛙以外,对方的 Collider2D,对方的碰撞器就是 other。
// 我们来判断
{
if (other.CompareTag("Water") && !isJump) // 如果青蛙是跳在水上了,并且没有在跳跃的情况下,其实就是游戏已经结束了
{
// --- snip ---
foreach(var hit in result) {
// 在刚刚的测试中,我们发现一个没有标签但是被我们输出的,我们可以实现输出的一下
// 假如在 foreach 的时候月循环了我们不想要的项目,我们可以怎么弄呢?——我们可以直接跳过判断
// 我们可以使用 continue 来跳过这个判断
if (hit.collider == null) {
// 很有可能是我们背景
continue; // 直接跳过当前的判断和循环
}
// 通过碰撞体返回标签
// Debug.Log(hit.collider.tag); // 这样我们就可以实时的看见碰撞到的物体
if (hit.collider.CompareTag("Wood"))
{
Debug.Log("在木板上");
}
}
}
// ---snip---
}
14. 跟随木板移动
private void OnTriggerStay2D(Collider2D other) // other:帮助我们更好理解,除了当前的青蛙以外,对方的 Collider2D,对方的碰撞器就是 other。
// 我们来判断
{
if (other.CompareTag("Water") && !isJump) // 如果青蛙是跳在水上了,并且没有在跳跃的情况下,其实就是游戏已经结束了
{
// 但是有可能是青蛙踩在了木板上,木板在水的上面。
// 所以,这个时候怎么办呢?
// 有一种极端的情况,就是青蛙跳到木板上,踩在木板的边缘,可能跟水有交接界,所以这个时候我们不容易判断。
Physics2D.RaycastNonAlloc(transform.position + Vector3.up * 0.1f, Vector2.zero, result); // 原点
// 循环出我们的标签
// foreach () // foreach 就是可以不用序号来实现循环,范围是什么,就是我们 result
foreach(var hit in result) {
// 在刚刚的测试中,我们发现一个没有标签但是被我们输出的,我们可以实现输出的一下
// 假如在 foreach 的时候月循环了我们不想要的项目,我们可以怎么弄呢?——我们可以直接跳过判断
// 我们可以使用 continue 来跳过这个判断
if (hit.collider == null) {
// 很有可能是我们背景
continue; // 直接跳过当前的判断和循环
}
// 通过碰撞体返回标签
// Debug.Log(hit.collider.tag); // 这样我们就可以实时的看见碰撞到的物体
if (hit.collider.CompareTag("Wood"))
{
// TODO: 跟随木板移动
Debug.Log("在木板上");
}
}
// 没有木板,游戏结束
}
if (other.CompareTag("Border") || other.CompareTag("Car")) // 比较标签// 这样我们就实现了,只要碰到其中一个游戏就 GameOver
{
// 我们暂时没有其他处理,直接输出调试信息即可
Debug.Log("Game Over!");
}
// 我们是怎么判断青蛙是否跳跃?——isJump,
// 那是什么时候改变 isJump 的状态那?——我们触发跳跃动画的时候,就是 isJump = true; 代表在天上
// 落地之后,我们在这个动画倒数第二帧,isJump = false; 也就是不再在空中跳跃了
// 所以,我们可以借助我们的布尔值的判断、标签的判断,来实现这个功能:看看我们是不是飞跃了障碍物。还是被障碍物撞到了。
// 开始写代码
if (!isJump && other.CompareTag("Obstacle")) // 如果我们的青蛙不是在跳跃的时候,并且碰到了 Obstacle
{
Debug.Log("Game Over! Obstacle"); // 那么游戏就结束了
}
}
你们可以自己添加花花草草。
15. 目前的完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
// 我们只需要在内部使用,所以直接写在里面
private enum Direction
{
// 可以写上这个类,包含哪些内容
Up, Right, Left
}
// 创建枚举之后,我们也需要创建对应枚举的变量
private Direction dir; // 现在发现有个红色报错,原因是上面我们使用了 private 没有办法把它暴露出来
// 组件一般写在上面
// 一般用两种方法: 一种就是 public 但是不推荐,因为 public 实现,需要我们自己去 Unity 里面去拖拽
// private 我们可以实现获取 Frog 自身身上的 Rigidbody2D 组件
private Rigidbody2D rb;
private Animator anim;
private SpriteRenderer sr;
public float jumpDistance;
private float moveDistance; // 真实跳跃距离
private bool buttonHeld; // 代表是否长按
private Vector2 destination; // 用来存储计算的值
private Vector2 touchPosition; // 存储屏幕的像素值
private bool isJump;
// 判断碰撞检测返回的物体
// private RaycastHit2D[] result; // 这是一个数组,使用数组需要初始化
// 所以,我们可以一开始就就进行初始化:
private RaycastHit2D[] result = new RaycastHit2D[2]; // 这是一个数组,使用数组需要初始化, 2是数组大小
private bool canJump;
// 我们要在我们的游戏最最开始第一帧执行,那么有一个周期函数是在 start 函数之前执行的,也就是 Awake
// 「Unity 为我们提供好的周期代码函数」
private void Awake() // 会在 start 之前执行
{
rb = GetComponent<Rigidbody2D>(); // 获得自身身上的组件
anim = GetComponent<Animator>(); // 获取 Animator 组件
sr = GetComponent<SpriteRenderer>();
}
private void Update()
{
// isJump 什么时候变成 flase 呢?
// FIXME:临时操作
// if (transform.position.y == destination.y)
// if (destination.y - transform.position.y < 0.1f)
// {
// isJump = false;
// }
if (canJump)
{
TriggerJump();
}
}
// 我们前面说了,如果你想使用物理的话,我们需要在 FixedUpdate 里面执行
private void FixedUpdate()
// FixedUpdate 是固定每 0.02s 执行一次,它不会依照你系统的快慢来执行——所以它是一个非常稳定的物理系统
{
// 在这里吗我们要实现什么呢?——实现真实的移动
// rb.position = Vector2.Lerp(起始坐标, 最终的坐标); // Linearly interpolates between vectors a and b by t.// 通过 t 在向量 a 和 b 之间进行线性插值。
// rb.position = Vector2.Lerp(transform.position, 那么最终坐标是?); // 目前不知道最终坐标位置,先注释掉
if (isJump) // 如果正在跳跃,则进行计算
{
rb.position = Vector2.Lerp(transform.position, destination, 0.134f); // 目前不知道最终坐标位置,先注释掉
// 不过我们用 moveDistance,我们用现在的坐标+moveDistance不就是移动的目标坐标
}
}
private void OnTriggerStay2D(Collider2D other) // other:帮助我们更好理解,除了当前的青蛙以外,对方的 Collider2D,对方的碰撞器就是 other。
// 我们来判断
{
if (other.CompareTag("Water") && !isJump) // 如果青蛙是跳在水上了,并且没有在跳跃的情况下,其实就是游戏已经结束了
{
// 但是有可能是青蛙踩在了木板上,木板在水的上面。
// 所以,这个时候怎么办呢?
// 有一种极端的情况,就是青蛙跳到木板上,踩在木板的边缘,可能跟水有交接界,所以这个时候我们不容易判断。
Physics2D.RaycastNonAlloc(transform.position + Vector3.up * 0.1f, Vector2.zero, result); // 原点
// 循环出我们的标签
// foreach () // foreach 就是可以不用序号来实现循环,范围是什么,就是我们 result
foreach(var hit in result) {
// 在刚刚的测试中,我们发现一个没有标签但是被我们输出的,我们可以实现输出的一下
// 假如在 foreach 的时候月循环了我们不想要的项目,我们可以怎么弄呢?——我们可以直接跳过判断
// 我们可以使用 continue 来跳过这个判断
if (hit.collider == null) {
// 很有可能是我们背景
continue; // 直接跳过当前的判断和循环
}
// 通过碰撞体返回标签
// Debug.Log(hit.collider.tag); // 这样我们就可以实时的看见碰撞到的物体
if (hit.collider.CompareTag("Wood"))
{
// TODO: 跟随木板移动
Debug.Log("在木板上");
}
}
// 没有木板,游戏结束
}
if (other.CompareTag("Border") || other.CompareTag("Car")) // 比较标签// 这样我们就实现了,只要碰到其中一个游戏就 GameOver
{
// 我们暂时没有其他处理,直接输出调试信息即可
Debug.Log("Game Over!");
}
// 我们是怎么判断青蛙是否跳跃?——isJump,
// 那是什么时候改变 isJump 的状态那?——我们触发跳跃动画的时候,就是 isJump = true; 代表在天上
// 落地之后,我们在这个动画倒数第二帧,isJump = false; 也就是不再在空中跳跃了
// 所以,我们可以借助我们的布尔值的判断、标签的判断,来实现这个功能:看看我们是不是飞跃了障碍物。还是被障碍物撞到了。
// 开始写代码
if (!isJump && other.CompareTag("Obstacle")) // 如果我们的青蛙不是在跳跃的时候,并且碰到了 Obstacle
{
Debug.Log("Game Over! Obstacle"); // 那么游戏就结束了
}
}
#region INPUT 输入回调函数
public void Jump(InputAction.CallbackContext context)
{
// 创建一个默认的函数写法
// public 公开的,其它类都可以调用
// void 没有返回类型
// if (context.phase == InputActionPhase.Performed)
// 下面是简写
// if (context.performed && isJump == false)
// TODO: 执行跳跃,跳跃的距离,记录分数,播放跳跃的音效
if (context.performed && !isJump) // 要执行跳跃,那前提是当前的青蛙没有跳跃
{ // 这样只有在功能完全的输出,我们才有里面的内容
// Debug.Log("Jump! Hello..." + context);
// 也改成具体跳跃的距离,方便后期调试
moveDistance = jumpDistance;
// Debug.Log("JUMP!" + " " + moveDistance); // 可以先注释掉了,不然控制台太乱
// destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// isJump = true;
canJump = true;
}
}
public void LongJump(InputAction.CallbackContext context)
{
if (context.performed && !isJump) // 要执行跳跃,那前提是当前的青蛙没有跳跃
{
moveDistance = jumpDistance * 2; // 小跳执行的话,那就是 jumpDistance
// Debug.Log("LONG JUMP!" + " " + moveDistance);
buttonHeld = true; // 一旦被长按了,我们的 buttonHeld 就为 true
}
// canceled 取消了
if (context.canceled && buttonHeld && !isJump) // 既要是被按下松开 context.canceled && 也要是 true buttonHeld
{
// 松掉空格「按键」
// TODO: 执行跳跃,而我们说了,要在松掉键盘,执行。那么把上面的 29 行代码,移动下来:
// Debug.Log("LONG JUMP!" + " " + moveDistance); // 可以先注释掉了,不然控制台太乱
buttonHeld = false; // 把状态改回来
// destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// isJump = true;
canJump = true;
}
}
public void GetTouchPosition(InputAction.CallbackContext context)
{
// Debug.Log("哈哈哈哈哈哈哈");
if (context.performed)
{
Debug.Log(context.ReadValue<Vector2>()); // 这也就是我们刚刚设置好的
touchPosition = Camera.main.ScreenToWorldPoint(context.ReadValue<Vector2>());
// Debug.Log(touchPosition); // 先注释掉
// 首先我们创建一个临时的变量,用来存储:获得屏幕点按,返回的值。和我们青蛙坐标的差值
// var 代表临时的变量 variable ,起名 offset 也就是位移,值如:touchPosition - transform.position
// 但是这两个数据类型不同,也就是 Vector2 和 Vector3 不能相减,所以需要强制转换:(Vector3)touchPosition
var offset = ((Vector3)touchPosition - transform.position).normalized; // normalized 进行向量化,也就是让它趋于 0 和 1 之间,把它趋于 1。「所以,这个值只能返回 0 到 1 之间」
// 所以当 offset > 0.7 的时候,就是百分之 70,相当于你偏移的位置超过 70% 了,那么我就判断你是向左或者向右。
// 我们可以想象一下,这个 offset.x 的值,可能是多少?
// 如果点在左边 offset.x 可能是个负值,如果点在屏幕的右侧,代表它是一个正值。但是无论它是正值也好、负值也好。
// 如果它的差值超过 70%,我都要它向左或者向右的方向。
// 不过换一个方向思考:如果它点按的位移差,小于 70%,我都判断它向正上方移动。
// 也代表这个值的绝对值,<= 0.7f「f 代表 float 浮点数」
// if (offset.x <= 0.7f) {}
// 那么我们,怎么判断它距离的位移差呢?——使用数学的方法,绝对值。数学的方法都在 Mathf 里面
if (Mathf.Abs(offset.x) <= 0.7f) // 这里是绝对值,去掉绝对值则需要考虑正负数
{
dir = Direction.Up;
}
else if (offset.x < 0) // 上面的条件不成立的话,表明 offset.x 绝对值大于 0.7f。那么接着我们就需要判断差值是否大于 0
{
dir = Direction.Left;
}
else if (offset.x > 0)
{
dir = Direction.Right;
}
}
}
#endregion
/// <summary>
/// 触发执行跳跃动画
/// </summary>
private void TriggerJump()
{
canJump = false;
//TODO:获得移动方向、播放动画
switch (dir)
{
case Direction.Up:
// TODO: 触发切换左右方向动画
anim.SetBool("isSide", false); // 向上的时候 isSide 设置为 false
destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// 就算是正常面向前面的话,也就是全部设置为 1
transform.localScale = Vector3.one; // 代表三个全部都是 1
// sr.flipX = true;
break;
case Direction.Right:
anim.SetBool("isSide", true);
destination = new Vector2(transform.position.x + moveDistance, transform.position.y);
transform.localScale = new Vector3(-1, 1, 1);// 向右则就是需要新建一个实例,x 设为1,因为我们需要翻转
break;
case Direction.Left:
anim.SetBool("isSide", true);
destination = new Vector2(transform.position.x - moveDistance, transform.position.y);
transform.localScale = Vector3.one; // 代表三个全部都是 1,向左的话保持不变即可,因为我们本来就有动画
break;
}
anim.SetTrigger("Jump"); // 和在 unity 里面设置的参数名称一致才可以,大小写要一致
// anim.SetBool();
// anim.SetFloat();
// anim.SetInteger();
}
#region Animation Event
public void JumpAnimationEvent() // 跳跃的动画事件
{
// 改变状态
isJump = true;
Debug.Log(dir);
// switch (dir)
// {
// case Direction.Up:
// destination = new Vector2(transform.position.x, transform.position.y + moveDistance);
// break;
// case Direction.Right:
// destination = new Vector2(transform.position.x + moveDistance, transform.position.y);
// break;
// case Direction.Left:
// destination = new Vector2(transform.position.x - moveDistance, transform.position.y);
// break;
// }
// 修改排序图层
sr.sortingLayerName = "Front";
}
// 还需要另外一个状态,也就是跳跃结束的时候,状态改为 false
public void FinishJumpAnimationEvent()
{
isJump = false;
// 修改排序图层
sr.sortingLayerName = "Middle";
}
#endregion
}
欢迎关注我公众号:AI悦创,有更多更好玩的等你发现!
公众号:AI悦创【二维码】
AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Linux、Web全栈」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh
- 0
- 0
- 0
- 0
- 0
- 0