13-随机生成物体
你好,我是悦创。
我先快速给你演示一下另外的两个汽车:
调整到和影子差不多大小即可
- Car01: 2.1
- Car02: 2.5
- Car03: 3
三种车,设置三种不同的速度。
在这节课中,我们要学会如何生成一个物体。并且可以随机生成三种车中的一种。
1. 编写代码
我们要编写代码,绑定在我们的 SpawnPoint A、SpawnPoint B,包括我们未来的小木板,也用同样的方式生成。
1.1 新建代码
新建我们的代码:
同时选择我们的 SpawnPoint A、SpawnPoint B,同时添加我们的脚本:
1.2 开始编写
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour
{
// 为了,让我们能获得,这所有三种小汽车的 prefabs。然后,让它们生成在场景中。
// 所以,我们要创建一个列表来保存我们的小汽车。
// 所以我们先创建列表 <表示要拿到的类型>
public List<GameObject> spawnObjects;
}
编写好代码后,我们回到我们的 Unity,查看一下。
1.3 添加预制体角色
添加有三种方法,我来一一演示给你。
1.3.1 方法1
显然,不是很方便。看看下一个方法。
1.3.2 方法2
以此类推操作。
1.3.3 方法三
接下来,这个方法可以同时选择多个预制体,拖入 Spawn Objects。
但是,这个选择多个预制体之后,右边的显示框会跑到别的地方。
接着,我们回到代码中继续编写。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour
{
// --- snip ---
private void Start() {
Spawn();
}
// 我们接下来要编写一个生成的方法
private void Spawn()
{
// 在这个函数中,我们要学会如何生成物体
// 我们先试一试生成列表中的第一个
// Instabtiate(角色, 生成的位置,是否旋转,生成之后成为当前物体的子物体)
// transform.position 生成到当前角色的 position,因为我们的代码是绑定在 SpawnPoint 生成点上
// 不需要任何旋转的话,我们使用:Quaternion.identity,意思是保留当前的状态,不使用任何的旋转
// spawnObjects[0] 找到序号为 0 的角色
Instantiate(spawnObjects[0], transform.position, Quaternion.identity, transform); // 放到 Start 中执行一下
}
}
我们一运行游戏,就会执行 Start 的方法,执行之后就是调用 Spawn。
1.4 测试运行
我们可以看到,它们都同时生成了,并且生成到了它们的子物体。
2. 随机生成小汽车
现在,我们就可以生成一个物体了,现在我们希望随机生成小汽车,应该怎么办呢?
我们需要一个随机的方法,随机的方法就是 Random。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour
{
// --- snip ---
private void Spawn()
{
// --- snip ---
var index = Random.Range(0, spawnObjects.Count); // 浮点数左闭右闭,整数:左闭右开 , 使用我们 list 列表的个数 spawnObjects.Count
Instantiate(spawnObjects[index], transform.position, Quaternion.identity, transform); // 放到 Start 中执行一下
}
}
我们可以测试一下:
我们可以发现,小车只生成一次,并没有重复生成。
3. 循环实现随机小汽车生成
我们如何在随机时间中,循环生成小汽车呢?
我们首先注释或者去掉 Spawn();
private void Start() {
// Spawn();
// 我们用另外一个方法来实现,也是 unity 自带的
// InvokeRepeating(); // 它在我们一开始的时候,就执行我们的方法
// InvokeRepeating(string的方法名, 从几时开始,重复的频率、间隔多少秒、随机间隔);
}
修改后的代码如下:
private void Start() {
// Spawn();
// 我们用另外一个方法来实现,也是 unity 自带的
// InvokeRepeating(); // 它在我们一开始的时候,就执行我们的方法
// InvokeRepeating(string的方法名, 从几时开始,重复的频率、间隔多少秒、随机间隔);
// nameof 为了让我们输入的时候有提示,确保我们不会写错名称
InvokeRepeating(nameof(Spawn), 0.2f, Random.Range(5f, 7f)); // 我希望游戏开始 0.2s 后开始 // 间隔时间是随机5s或7s的时间内
}
我们执行测试一下:
虽然上面实现了,但是我们可以发现小汽车是同一个方向生成的,这样的情况下,我们的小青蛙是不可能取胜的。
我希望有一排车道是反方向行驶的。
4. 反方向汽车
现在我们运行游戏,小汽车还是会向右运行程序。
我们如何实现向左移动呢?——这个时候我们需要暴露一个参数来实现向左向右。可以想想我们之前青蛙是怎么实现的。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour
{
public int direction;
// --- snip ---
}
向左改为 -1,向右改为 1。
那,这个参数,每次是怎么告诉我们的小汽车呢?
我们生成之后,我们要在我们的代码中调用它,告诉小汽车移动方向。
// file: MoveForward.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveForward : MonoBehaviour
{
// 首先我们需要三种小汽车有不同的速度
public float speed; // 方便我们在界面操作设置速度
public int dir; // 用来记录小气的方向,也是小汽车移动的方向
private Vector2 startPos; // 用于记录我们初始坐标的位置
// --- snip ---
}
// file: MoveForward.cs
private void Move() {
// --- snip ---
transform.position += transform.right * dir * speed * Time.deltaTime;
// TODO: 我们乘以速度的同时,也要乘以它的方向 dir * speed
}
// file: MoveForward.cs
private void Start()
{
// --- snip ---
// 设置完成下面移动方向,接下来设置我们角色的方向
transform.localScale = new Vector3(dir, 1, 1);
}
// file: Spawner.cs
public class Spawner : MonoBehaviour
{
public int direction;
private void Spawn()
{
// --- snip ---
// 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
}
}
现在可以测试运行效果,并且你也会发现,青蛙碰到小汽车后也会出现 GameOver:
现在,你可以自由发挥想象力,设置自己的马路,花花草草都可以。
别忘记设置好后,保存到 Terrain 做成预制体。
其实,到这里。你已经有足够的知识来生成这些随机的场景了。
我们用的方法,就是 Instantiate,知道要生成的是哪个角色。
5. 截止目前的完整代码
// file: Assets/Scripts/Camera/CameraControl.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraControl : MonoBehaviour
{
public Transform frog; // 这个创建的类型,取决于我们想要用到 Unity 身上的哪些组件。
// public GameObject frog; // 如果使用 GameObject,我们就需要 GameObject.Transform 然后获取坐标,这样显然太复杂而麻烦。
// 拿到 Transform 坐标之后呢?我们需要实时的跟随。
// 作为摄像机跟随,如果跟青蛙保持在同一个更新的频率上,很可能会造成画面的抖动。
// 因为,Update 是根据不同设备来进行更新的。
// 所以,我希望在 Frog 更新它的坐标的下一帧,然后才更新相机的坐标位置。
// 所以,我们使用周期函数 LateUpdate
public float offsetY;
// unity 中可以通过内置的 Screen,就可以获取得到它的宽和它的高
private float ratio; // 用来计算比例
public float zoomBase; // 我们需要一个基础的比例
private void Start()
{
ratio = (float)Screen.height / (float)Screen.width;
// Debug.Log(ratio);
// 我们既然有这个比例,用基础的数值,乘以比例、乘以一半
Camera.main.orthographicSize = zoomBase * ratio * 0.5f; // 使用这个代码就可以获取 Unity 当中被标记为 Main Camera 的主相机
}
private void LateUpdate()
{
// 在这里更新相机的 position;
transform.position = new Vector3(transform.position.x, frog.transform.position.y + offsetY * ratio, transform.position.z); // x and z 是保持不变。y 轴要有一个位移差。
// 如果 y 轴完全等于我们青蛙的坐标会怎么样呢?「讲义有写」
}
}
// file: Assets/Scripts/Frog/PlayerController.cs
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 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("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
}
// file: Assets/Scripts/GamePlay/MoveForward.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveForward : MonoBehaviour
{
// 首先我们需要三种小汽车有不同的速度
public float speed; // 方便我们在界面操作设置速度
public int dir; // 用来记录小气的方向,也是小汽车移动的方向
private Vector2 startPos; // 用于记录我们初始坐标的位置
// 在什么时候开始记录呢?
// 在我们 Start 的时候记录
private void Start()
{
startPos = transform.position; // 这样我们就拥有了初始坐标了
// 设置完成下面移动方向,接下来设置我们角色的方向
transform.localScale = new Vector3(dir, 1, 1);
}
// Update is called once per frame
void Update() // 持续每帧在运行的,在运行的过程当中我要判断它的坐标
// 就是在小汽车移动的初始坐标基础上,小汽车移动的 x 的差值超过了我们的范围,比如我们指定了一个宽度。
// 那么我们就销毁,所以我需要知道我们小汽车初始位置。
// 因为随着我们的场景不断地更新,不断地添加,它生成的坐标 x 和 y 不是固定的。
{
// 接下来,我们需要在 Update 中判断一下:
if (Mathf.Abs(transform.position.x - startPos.x) > 25)
{
Destroy(this.gameObject); // 把当前的 gameObject 进行销毁 // Destroy 固定函数,移除场景中的 gameObject
}
// 在这里我们调用函数
Move();
}
// 我希望我的小汽车不断的运行移动,我们来写一个方法
private void Move() {
// 我们想想,小汽车要怎么移动呢?
// 所谓的速度,它需要不断的在 update 里面去调用,使每一帧都向前前进一些,那前进的就是 speed
// 也就是我在我当前坐标的基础之上,让它 x 的方向,不断的添加的 speed
// 我们使用 transform,transform.position = transform.position + transform.right 和下面等价
// transform.position += transform.right * speed * Time.deltaTime; // transform.right 当前角色的纵坐标的向右方向,我们要在这个基础之上乘以 speed,
transform.position += transform.right * dir * speed * Time.deltaTime; // transform.right 当前角色的纵坐标的向右方向,我们要在这个基础之上乘以 speed,
// TODO: 我们乘以速度的同时,也要乘以它的方向 dir * speed
// 接着我们乘以我们的时间修正,因为,我们之前说过 Update 是每帧执行,但是在不同设备上有不同的帧数,所以在这里我们会乘以一个时间修正。这个是固定写法 Time.deltaTime
// 相当于我们角色的方向,向右的意思
}
}
// file: Assets/Scripts/GamePlay/Spawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour
{
public int direction;
// 为了,让我们能获得,这所有三种小汽车的 prefabs。然后,让它们生成在场景中。
// 所以,我们要创建一个列表来保存我们的小汽车。
// 所以我们先创建列表 <表示要拿到的类型>
public List<GameObject> spawnObjects;
private void Start() {
// Spawn();
// 我们用另外一个方法来实现,也是 unity 自带的
// InvokeRepeating(); // 它在我们一开始的时候,就执行我们的方法
// InvokeRepeating(string的方法名, 从几时开始,重复的频率、间隔多少秒、随机间隔);
// nameof 为了让我们输入的时候有提示,确保我们不会写错名称
InvokeRepeating(nameof(Spawn), 0.2f, Random.Range(5f, 7f)); // 我希望游戏开始 0.2s 后开始 // 间隔时间是随机5s或7s的时间内
}
// 我们接下来要编写一个生成的方法
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
}
}
欢迎关注我公众号:AI悦创,有更多更好玩的等你发现!
公众号:AI悦创【二维码】
AI悦创·编程一对一
AI悦创·推出辅导班啦,包括「Python 语言辅导班、C++ 辅导班、java 辅导班、算法/数据结构辅导班、少儿编程、pygame 游戏开发、Linux、Web全栈」,全部都是一对一教学:一对一辅导 + 一对一答疑 + 布置作业 + 项目实践等。当然,还有线下线上摄影课程、Photoshop、Premiere 一对一教学、QQ、微信在线,随时响应!微信:Jiabcdefh
C++ 信息奥赛题解,长期更新!长期招收一对一中小学信息奥赛集训,莆田、厦门地区有机会线下上门,其他地区线上。微信:Jiabcdefh
方法一:QQ
方法二:微信:Jiabcdefh
- 0
- 0
- 0
- 0
- 0
- 0