击球方阵

乒乓克隆

  • 运用立方体制造竞技场、球拍和球。
  • 移动球和球拍。
  • 击球并得分。
  • 让相机感受到冲击力。
  • 给游戏一个笼统的霓虹灯外观。

这是有关根底游戏的系列教程中的榜首个教程。在其间,咱们将创立一个简略的 Pong 克隆。

本教程是运用 Unity 2021.3.16f1 制造的。

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

本系列将涵盖简略游戏根底游戏的创立,以展现如安在短时刻内将主意转变为最小的作业游戏。这些游戏将是克隆的,所以咱们不用从头开端创造一个新主意,但咱们会以某种办法偏离规范。

除了坚持简略之外,咱们还将为这个系列设置一个规划约束来约束自己:咱们只能烘托默许的立方体和国际空间文本,仅此而已。另外,我不包括声响。

本系列假定您至少现已完结了根底知识系列,以及您选择的更多系列,以便您了解 Unity 中的作业和编程。我不会像其他系列那样具体地展现每个步骤,假定您能够自己创立游戏方针并在查看器中连接内容而无需屏幕截图。我也不会解说基本的数学或运动定律。

游戏场景

咱们将在本教程中克隆的游戏是 Pong,这是乒乓球或网球的十分笼统的表明办法。你能够克隆 Pong 的主意,但不要给它一个类似的姓名,以免会要求你把它拿下来。所以咱们把咱们的游戏命名为击球方阵,由于它都是正方形。

咱们将从新 3D 项目的默许示例场景开端,虽然重命名为 .咱们将运用的唯一包是您选择的编辑器集成包(在我的比如中)及其依靠项。这些包的依靠项是 Burst, Core RP Library, Custom NUnit, Mathematics, Seacher, Shader Graph, Test Framework 和 Unity UI。

创立烘托/URP 资源(运用通用烘托器)资源,并将其用于项目设置中的图形/可编程烘托管线设置和质量/烘托管线资源。

竞技场

创立默许多维数据集并将其转换为预制件。移除它的对撞机,由于咱们不会依靠物理系统。运用其间四个在一个维度上缩放到 20 个单位,以形成 XZ 平面上原点周围 2020 个正方形区域的鸿沟。由于立方体的厚度为一个单位,因而每个立方体在恰当的方向上从原点移动 10.5 个单位。 [外链图片转存失利,源站或许有防盗链机制,建议将图片保存下来直接上传(img-f0XUScMH-1682677243478)(null)]

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

为了使它更风趣一些,请再增加四个扩大到巨细 2 的预制件实例,并运用它们来填充鸿沟角。将其 Y 坐标设置为 0.5,以便所有底部对齐。一起调整主摄像头,使其显现整个竞技场的自上而下视图。

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

竞技场需求有两个桨(板)。创立另一个默许多维数据集并将其转换为新的预制件,同样没有碰撞体。将其比例设置为 (8, 1.1, 1.1) 并为其供给白色原料。将它的两个实例增加到场景中,以便它们与底部和顶部鸿沟的中间堆叠,如上所示。

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

咱们最不需求的是一个球,它也将是一个立方体。为此创立另一个立方体预制件(以防您以后决议增加多个球),并为其供给黄色原料。将它的实例放在场景的中心。

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

组件

虽然咱们的游戏十分简略,能够用一个脚本操控一切,但咱们会在逻辑上拆分功用,以使代码更易于了解。咱们现在将创立明显的组件类型,稍后再填充它们。

首先,咱们有一个移动的球,因而创立一个扩展类并将其作为组件增加到球预制件中。**Ball**``MonoBehaviour

using UnityEngine;
public class Ball : MonoBehaviour {}

其次,咱们有会尝试击球的球拍,因而请为它们创立一个组件类型并将其增加到球拍预制件中。**Paddle**

using UnityEngine;
public class Paddle : MonoBehaviour {}

第三,咱们需求一个操控游戏循环并与球和球拍通讯的组件。只需命名它,给它装备字段以连接球和两个球拍,将其附加到场景中的空游戏方针,然后连接起来。**Game**

using UnityEngine;
public class Game : MonoBehaviour
{
[SerializeField]
Ball ball;
[SerializeField]
Paddle bottomPaddle, topPaddle;
}

操控球

比赛的要点在于操控球。球在竞技场上直线移动,直到击中某物。每个球员都试图定位其球拍,使其击中球并将其弹回另一侧。

方位和速度

为了移动,需求盯梢它的方位和速度。由于这实际上是一个 2D 游戏,咱们将为此运用字段,其间 2D Y 维度表明 3D Z 维度。咱们从恒定的 X 和 Y 速度开端,可经过单独的可序列化浮点字段进行装备。我运用 8 和 10 作为默许值。**Ball**``Vector2

public class Ball : MonoBehaviour
{
[SerializeField, Min(0f)]
float
constantXSpeed = 8f,
constantYSpeed = 10f;
Vector2 position, velocity;
}

咱们终究或许会在每次更新时对球的方位和速度进行各种调整,所以咱们不要一向设置它。相反,请为此创立一个公共办法。Transform.localPosition``UpdateVisualization

public void UpdateVisualization () =>
transform.localPosition = new Vector3(position.x, 0f, position.y);

咱们不会让球不会自行移动,而是经过公共办法让它履行规范运动。Move

public void Move () => position += velocity * Time.deltaTime;

咱们还为它供给了一个公共办法,为新游戏做好预备。球从竞技场的中心开端,更新其可视化作用以匹配,并运用装备的速度。由于底部球拍将由玩家操控,因而将速度的 Y 重量设置为负数,使其首先向玩家移动。StartNewGame

public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity = new Vector2(constantXSpeed, -constantYSpeed);
}

现在能够操控球了。至少,当它唤醒球时,球应该开端一个新的游戏,当它更新时,球应该移动,然后更新它的可视化。**Game**

void Awake () => ball.StartNewGame();
void Update ()
{
ball.Move();
ball.UpdateVisualization();
}

跳出鸿沟

此刻,咱们有一个球在进入游戏形式后开端移动,而且继续前进,穿过底部鸿沟并消失在视界之外。 不直接知道竞技场的鸿沟,咱们会坚持这种状态。相反,咱们将向其增加两个公共办法,这些办法强制在给定鸿沟的单个维度中反弹。咱们仅仅假定退回请求是合适的。**Ball**

当某个鸿沟被越过时,就会产生反弹,这意味着球现在超出了鸿沟。这有必要经过反映其轨道来纠正。终究方位仅仅等于鸿沟减去当时方位的两倍。此外,该维度中的速度会翻转。这些反弹是完美的,因而另一个维度的方位和速度不受影响。创立和完结此目的的办法。BounceX``BounceY

public void BounceX (float boundary)
{
position.x = 2f * boundary - position.x;
velocity.x = -velocity.x;
}
public void BounceY (float boundary)
{
position.y = 2f * boundary - position.y;
velocity.y = -velocity.y;
}

当球的边际触摸鸿沟而不是其间心时,就会产生恰当的反弹。因而,咱们需求知道球的巨细,为此咱们将增加一个以规模表明的装备字段,默许情况下设置为 0.5,与单位立方体匹配。

[SerializeField, Min(0f)]
float
constantXSpeed = 8f,
constantYSpeed = 10f,
extents = 0.5f;

球自身不会决议何时反弹,因而其规模和方位有必要可公开拜访。为此增加 getter 属性。

public float Extents => extents;
public Vector2 Position => position;

**Game**还需求知道竞技场的规模,能够是以原点为中心的任何矩形。为此指定一个装备字段,默许情况下设置为 1010。Vector2

[SerializeField, Min(0f)]
Vector2 arenaExtents = new Vector2(10f, 10f);

咱们首先查看 Y 维度。为此创立一个办法。要查看的规模等于竞技场 Y 规模减去球规模。假如球低于负规模或高于正规模,则它应该从恰当的鸿沟反弹。在移动球和更新其可视化作用之间调用此办法。BounceYIfNeeded

void Update ()
{
ball.Move();
BounceYIfNeeded();
ball.UpdateVisualization();
}
void BounceYIfNeeded ()
{
float yExtents = arenaExtents.y - ball.Extents;
if (ball.Position.y < -yExtents)
{
ball.BounceY(-yExtents);
}
else if (ball.Position.y > yExtents)
{
ball.BounceY(yExtents);
}
}

球现在从底部和顶部边际反弹。要一起从左右边际反弹,请以相同的办法创立一个办法,但针对 X 维度并在 .BounceXIfNeeded``BounceYIfNeeded

void Update ()
{
ball.Move();
BounceYIfNeeded();
BounceXIfNeeded();
ball.UpdateVisualization();
}
void BounceXIfNeeded ()
{
float xExtents = arenaExtents.x - ball.Extents;
if (ball.Position.x < -xExtents)
{
ball.BounceX(-xExtents);
}
else if (ball.Position.x > xExtents)
{
ball.BounceX(xExtents);
}
}

球现在被竞技场所包含,从边际反弹,永远不会逃脱。

移动桨

咱们还需求知道桨的规模和速度,因而将它们的装备字段增加到 ,默许情况下设置为 4 和 10。**Paddle**

public class Paddle : MonoBehaviour
{
[SerializeField, Min(0f)]
float
extents = 4f,
speed = 10f;
}

**Paddle**还获取一个公共办法,这次运用方针和竞技场规模的参数,两者都在 X 维度中。让它最初取得方位,夹紧 X 坐标,使桨不能移动超越应有的方位,然后设置其方位。Move

public void Move (float target, float arenaExtents)
{
Vector3 p = transform.localPosition;
float limit = arenaExtents - extents;
p.x = Mathf.Clamp(p.x, -limit, limit);
transform.localPosition = p;
}

球拍应该由玩家操控,但有两种玩家:人工智能和人类。让咱们首先完结一个简略的 AI 操控器,办法是创立一个获取 X 方位和方针并回来新 X 的办法。假如它在方针的左侧,它仅仅以最大速度向右移动,直到它与方针匹配,否则它以相同的办法向左移动。这是一个没有任何预测的愚蠢反应式 AI,它的难度只取决于它的速度。AdjustByAI

float AdjustByAI (float x, float target)
{
if (x < target)
{
return Mathf.Min(x + speed * Time.deltaTime, target);
}
return Mathf.Max(x - speed * Time.deltaTime, target);
}

关于人类玩家,咱们创立了一个不需求方针的办法,只需依据按下的箭头键向左或向右移动。假如一起按下两者,它将不会移动。AdjustByPlayer

float AdjustByPlayer (float x)
{
bool goRight = Input.GetKey(KeyCode.RightArrow);
bool goLeft = Input.GetKey(KeyCode.LeftArrow);
if (goRight && !goLeft)
{
return x + speed * Time.deltaTime;
}
else if (goLeft && !goRight)
{
return x - speed * Time.deltaTime;
}
return x;
}

现在增加一个切换开关来确认球拍是否由 AI 操控,并调用恰当的办法来调整方位的 X 坐标。Move

[SerializeField]
bool isAI;
…
public void Move (float target, float arenaExtents)
{
Vector3 p = transform.localPosition;
p.x = isAI ? AdjustByAI(p.x, target) : AdjustByPlayer(p.x);
float limit = arenaExtents - extents;
p.x = Mathf.Clamp(p.x, -limit, limit);
transform.localPosition = p;
}

在 的开头移动两个桨。**Game**.Update

void Update ()
{
bottomPaddle.Move(ball.Position.x, arenaExtents.x);
topPaddle.Move(ball.Position.x, arenaExtents.x);
ball.Move();
BounceYIfNeeded();
BounceXIfNeeded();
ball.UpdateVisualization();
}

桨现在要么呼应箭头键,要么自行移动。启用顶部桨的 AI,并将其速度降低到 5,因而很简略被击败。请注意,您能够在玩游戏时随时启用或禁用 AI。

玩游戏

现在咱们有了功用性球和球拍,咱们能够制造一个可玩的游戏。玩家尝试移动他们的球拍,以便他们将球弹回竞技场的另一侧。假如他们没有做到这一点,他们的对手就会得分。

击球

增加一个办法,该办法回来它是否在其当时方位击中球,给定其 X 方位和规模。咱们能够经过从球中减去球拍方位,然后将其除以球拍加球规模来查看这一点。结果是一个命中系数,假如球拍成功击中球,则在 -1-1 规模内的某个当地。HitBall``**Paddle**

public bool HitBall (float ballX, float ballExtents)
{
float hitFactor =
(ballX - transform.localPosition.x) /
(extents + ballExtents);
return -1f <= hitFactor && hitFactor <= 1f;
}

命中因子自身也很有用,由于它描绘了球相关于球拍中心和规模的击球方位。在乒乓球中,这决议了球从球拍上反弹的角度。因而,让咱们经过输出参数使其可用。

public bool HitBall (float ballX, float ballExtents, out float hitFactor)
{
hitFactor =
(ballX - transform.localPosition.x) /
(extents + ballExtents);
return -1f <= hitFactor && hitFactor <= 1f;
}

假如球在被球拍击中后速度产生变化,那么咱们简略的弹跳代码是不够的。咱们有必要将时刻倒退到反弹产生的那一刻,确认新的速度,并将时刻向前移动到当时时刻。

在 中,重命名并增加一个可装备的 ,默许情况下设置为 20。然后创立一个覆盖其当时办法的办法,给定起始方位和速度系数。新速度成为按因子缩放的最大速度,然后确认具有给定时刻增量的新方位。**Ball**``constantXSpeed``startSpeed``maxXSpeed``SetXPositionAndSpeed

[SerializeField, Min(0f)]
float
maxXSpeed = 20f,
startXSpeed = 8f,
constantYSpeed = 10f,
extents = 0.5f;
…
public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity = new Vector2(startXSpeed, -constantYSpeed);
}
public void SetXPositionAndSpeed (float start, float speedFactor, float deltaTime)
{
velocity.x = maxXSpeed * speedFactor;
position.x = start + velocity.x * deltaTime;
}

要找到反弹的切当时刻,有必要知道球的速度,因而请为其增加公共 getter 属性。

public Vector2 Velocity => velocity;

**Game**现在在 Y 维度上弹跳时有更多的作业要做。因而,咱们将首先调用一个新办法,而不是直接调用,其间包含防守桨的参数。ball.Bounce``**Game**.BounceY

void BounceYIfNeeded ()
{
float yExtents = arenaExtents.y - ball.Extents;
if (ball.Position.y < -yExtents)
{
BounceY(-yExtents, bottomPaddle);
}
else if (ball.Position.y > yExtents)
{
BounceY(yExtents, topPaddle);
}
}
void BounceY (float boundary, Paddle defender)
{
ball.BounceY(boundary);
}

有必要做的榜首件事是确认反弹产生的时刻。这是经过从球的 Y 方位减去鸿沟并将其除以球的 Y 速度来发现的。请注意,咱们疏忽了球拍比鸿沟粗一点,由于这仅仅一个视觉上的东西,以避免烘托时 Z 战役。BounceY

float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y;
ball.BounceY(boundary);

接下来,核算反弹产生时球的 X 方位。

float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y;
float bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce;

之后咱们履行原始的 Y 弹跳,然后查看防守球拍是否击中球。假如是这样,请依据弹跳 X 方位、命中系数以及它产生的时刻设置球的 X 方位和速度。

ball.BounceY(boundary);
if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}

在这一点上,咱们有必要考虑在两个维度上都产生反弹的或许性。在这种情况下,反弹的 X 方位或许终究位于竞技场之外。这能够经过先履行 X 反弹来避免,但仅在需求时。为了支撑此更改,因而它查看的 X 方位是经过参数供给的。BounceXIfNeeded

void Update ()
{
…
BounceXIfNeeded(ball.Position.x);
ball.UpdateVisualization();
}
void BounceXIfNeeded (float x)
{
float xExtents = arenaExtents.x - ball.Extents;
if (x < -xExtents)
{
ball.BounceX(-xExtents);
}
else if (x > xExtents)
{
ball.BounceX(xExtents);
}
}

然后,咱们还能够依据它抵达 Y 鸿沟的方位调用 in。因而,咱们只处理 X 反弹产生在 Y 反弹之前。之后再次核算反弹 X 方位,现在或许根据不同的球方位和速度。BounceXIfNeeded``BounceY

float durationAfterBounce = (ball.Position.y - boundary) / ball.Velocity.y;
float bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce;
BounceXIfNeeded(bounceX);
bounceX = ball.Position.x - ball.Velocity.x * durationAfterBounce;
ball.BounceY(boundary);

接下来,球的速度会依据它击中球拍的方位而变化。它的 Y 速度始终坚持不变,而它的 X 速度是可变的。这意味着从一个桨移动到另一个桨总是需求相同的时刻,但它或许会横向移动一点或许多。Pong 的球的行为办法相同。

与乒乓球不同的是,在咱们的游戏中,当球拍错失球拍时,球仍然会从竞技场的边际反弹,而在乒乓球中会触发新一轮。咱们的游戏仅仅不间断地进行,不会中断游戏玩法。让咱们将这种行为作为咱们游戏的共同古怪。

得分点

当防守球拍错失球时,对手得分。咱们将在地板或竞技场上显现两名球员的得分。为此创立一个文本游戏方针,经过 。这将触发一个弹出窗口,咱们从中选择选项 .

将文本转换为预制件。调整其宽度为 20,高度为 6,Y 方位为 −0.5,X 旋转为 90。为其组件指定起始文本 0、字体巨细 72,并将其对齐办法设置为居中和中间。然后创立它的两个实例,Z 方位为 −5 和 5。RectTransform``TextMeshPro

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

咱们认为玩家和它的球拍是一回事,因而将经过可装备的字段盯梢对其分数文本的引证。**Paddle**``TMPro.TextMeshPro

using TMPro;
using UnityEngine;
public class Paddle : MonoBehaviour
{
[SerializeField]
TextMeshPro scoreText;
…
}

它还将盯梢自己的商店。给它一个私有办法,用一个新的分数替换它当时的分数,并更新它的文本以匹配。这能够经过运用字符串和分数作为参数调用文本组件来完结。SetScore``SetText``"{0}"

int score;
…
void SetScore (int newScore)
{
score = newScore;
scoreText.SetText("{0}", newScore);
}

若要开端新游戏,请引进将分数设置为零的公共办法。此外,增加一个公共办法,该办法递增分数并回来这是否会导致玩家取胜。为了确认,给它一个取胜所需积分的参数。StartNewGame``ScorePoint

public void StartNewGame ()
{
SetScore(0);
}
public bool ScorePoint (int pointsToWin)
{
SetScore(score + 1);
return score >= pointsToWin;
}

**Game**现在也有必要在两个桨上调用,所以让咱们给它自己的办法来传递音讯,它在 .StartNewGame``StartNewGame``Awake

void Awake () => StartNewGame();
void StartNewGame ()
{
ball.StartNewGame();
bottomPaddle.StartNewGame();
topPaddle.StartNewGame();
}

使取胜的积分数量可装备,最小为 2,默许值为 3。然后将攻击者球拍作为第三个参数增加到其间,假如防守方没有击中球,则让它调用它。假如这导致攻击者取胜,则开端新游戏。BounceY``ScorePoint

[SerializeField, Min(2)]
int pointsToWin = 3;
…
void BounceYIfNeeded ()
{
float yExtents = arenaExtents.y - ball.Extents;
if (ball.Position.y < -yExtents)
{
BounceY(-yExtents, bottomPaddle, topPaddle);
}
else if (ball.Position.y > yExtents)
{
BounceY(yExtents, topPaddle, bottomPaddle);
}
}
void BounceY (float boundary, Paddle defender, Paddle attacker)
{
…
if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}
else if (attacker.ScorePoint(pointsToWin))
{
StartNewGame();
}
}

新游戏倒计时

与其当即开端新游戏,不如引进推延,在此期间能够欣赏终究分数。让咱们也推延游戏的初始开端,以便玩家能够做好预备。创立一个新的文本实例以在竞技场中心显现倒计时,其字体巨细减小到 32 并作为其初始文本。

【Unity小游戏】游戏开发案例-Unity打造畅玩无阻的小游戏(上)

为倒计时文本和新的游戏推延持续时刻供给装备字段,最小值为 1,默许值为 3。还要给它一个字段来盯梢新游戏之前的倒计时,并将其设置为推延持续时刻,而不是当即开端新游戏。**Game**``Awake

using TMPro;
using UnityEngine;
public class Game : MonoBehaviour
{
…
[SerializeField]
TextMeshPro countdownText;
[SerializeField, Min(1f)]
float newGameDelay = 3f;
float countdownUntilNewGame;
void Awake () => countdownUntilNewGame = newGameDelay;

咱们仍然总是移动球拍,这样玩家就能够在倒计时期间进入方位。将所有其他代码移动到一个新办法,咱们仅在倒计时为零或更小时才调用该办法。否则咱们调用 ,一种减少倒计时并更新其文本的新办法。Update``UpdateGame``UpdateCountdown

void Update ()
{
bottomPaddle.Move(ball.Position.x, arenaExtents.x);
topPaddle.Move(ball.Position.x, arenaExtents.x);
if (countdownUntilNewGame <= 0f)
{
UpdateGame();
}
else
{
UpdateCountdown();
}
}
void UpdateGame ()
{
ball.Move();
BounceYIfNeeded();
BounceXIfNeeded(ball.Position.x);
ball.UpdateVisualization();
}
void UpdateCountdown ()
{
countdownUntilNewGame -= Time.deltaTime;
countdownText.SetText("{0}", countdownUntilNewGame);
}

假如倒计时到达零,请停用倒计时文本并开端新游戏,否则更新文本。但是,让咱们只显现整秒钟。咱们能够经过倒计时的上限来做到这一点。要使初始文本可见,请仅在显现值小于装备的推延时才更改它。假如推延设置为整数,则文本将在榜首秒内可见。

countdownUntilNewGame -= Time.deltaTime;
if (countdownUntilNewGame <= 0f)
{
countdownText.gameObject.SetActive(false);
StartNewGame();
}
else
{
float displayValue = Mathf.Ceil(countdownUntilNewGame);
if (displayValue < newGameDelay)
{
countdownText.SetText("{0}", displayValue);
}
}

让咱们在没有比赛进行时躲藏球。由于在开发过程中让球在场景中处于活动状态很方便,因而咱们给出了一种自行停用的办法。然后在 结束时再次激活它。此外,还引进了一种公共办法,该办法将其 X 方位设置为状态的中心——因而 AI 将在游戏之间将其球拍移动到中间——并自行停用。**Ball**``Awake``StartNewGame``EndGame

void Awake () => gameObject.SetActive(false);
public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity = new Vector2(startXSpeed, -constantYSpeed);
gameObject.SetActive(true);
}
public void EndGame ()
{
position.x = 0f;
gameObject.SetActive(false);
}

也给出一个办法,当玩家取胜时调用,而不是当即开端新游戏。在其间,重置倒计时,将倒计时文本设置为并激活它,并告知球游戏结束。**Game**``EndGame

void BounceY (float boundary, Paddle defender, Paddle attacker)
{
…
if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}
else if (attacker.ScorePoint(pointsToWin))
{
EndGame();
}
}
void EndGame ()
{
countdownUntilNewGame = newGameDelay;
countdownText.SetText("GAME OVER");
countdownText.gameObject.SetActive(true);
ball.EndGame();
}

随机性

在这一点上,咱们有一个最小的功用游戏,但让咱们经过以两种不同的办法增加一些随机性来让它更风趣。首先,不要总是以相同的 X 速度开端,而是将可装备的最大发动 X 速度默许设置为 2,并在每场比赛开端时运用它来随机化其速度。**Ball**

[SerializeField, Min(0f)]
float
maxXSpeed = 20f,
maxStartXSpeed = 2f,
constantYSpeed = 10f,
extents = 0.5f;
…
public void StartNewGame ()
{
position = Vector2.zero;
UpdateVisualization();
velocity.x = Random.Range(-maxStartXSpeed, maxStartXSpeed);
velocity.y = -constantYSpeed;
gameObject.SetActive(true);
}

其次,给 AI 一个方针误差,这样它就不会总是试图将球击到它的切当中心。为了操控这一点,引进了一个可装备的最大定位误差,表明其规模的一小部分(类似于命中因子),默许情况下设置为 0.75。运用字段盯梢其当时误差,并增加随机化的办法。**Paddle**``ChangeTargetingBias

[SerializeField, Min(0f)]
float
extents = 4f,
speed = 10f,
maxTargetingBias = 0.75f;
…
float targetingBias;
…
void ChangeTargetingBias () =>
targetingBias = Random.Range(-maxTargetingBias, maxTargetingBias);

方针误差会改变每个新游戏以及球拍试图击球的时刻。

public void StartNewGame ()
{
SetScore(0);
ChangeTargetingBias();
}
public bool HitBall (float ballX, float ballExtents, out float hitFactor)
{
ChangeTargetingBias();
…
}

要应用误差,请在移动球拍之前将其增加到方针中。AdjustByAI

float AdjustByAI (float x, float target)
{
target += targetingBias * extents;
…
}

Unity小游戏开发实战:从零开端打造你的自己的游戏国际(上篇)

【Unity小游戏】游戏开发事例-Unity打造畅玩无阻的小游戏(下)