【Unityミニゲーム】ゲーム開発事例~Unityで自由に作れるミニゲーム(後編)

バッティングスクエア

ピンポンクローン

  • 立方体を使用してアリーナ、ラケット、ボールを構築します。
  • ボールとラケットを動かします。
  • ボールを打って得点を決めましょう。
  • カメラに衝撃を感じさせます。
  • ゲームに抽象的なネオンの外観を与えます。

これは、基本的なゲームに関する一連のチュートリアルの最初のものです。その中で、単純な Pong クローンを作成します。

このチュートリアルは Unity 2021.3.16f1 を使用して作成されました。

上記内容と併せて

格納パドル

ゲームの最終段階として、得点するたびにズームアウトしてみましょう。これにより、プレイヤーが勝利にどれだけ近づいているかに基づいてハンディキャップが作成されます。現在のスコープをプライベート フィールドに変換し、その最小値と最大値を構成可能にします。デフォルトでは両方とも 4 に設定されます。現在のエクステントを置き換えるメソッドが導入されました。これにより、ゲーム オブジェクトのローカル スケールも一致するように調整されます。**Paddle**``SetExtents

[SerializeField, Min(0f)]
float

minExtents = 4f,
maxExtents = 4f,
speed = 10f,
maxTargetingBias = 0.75f;

…

float extents, targetingBias;

…

void SetExtents (float newExtents)
{
extents = newExtents;
Vector3 s = transform.localScale;
s.x = 2f * newExtents;
transform.localScale = s;
}

ストロークの勝利にどれだけ近づいているかに基づいて、ストロークの終わりの範囲を設定します。これは、新しいスコアを勝利に必要なスコアから 1 を引いた値で割った値に基づいて、最大範囲から最小範囲まで補間することによって行われます。これに必須のパラメータを追加します。ゼロに設定されている場合、デフォルトでは 1 より大きい任意の値を指定できます。SetScore

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

…

void SetScore (int newScore, float pointsToWin = 1000f)
{
score = newScore;
scoreText.SetText("{0}", newScore);
SetExtents(Mathf.Lerp(maxExtents, minExtents, newScore / (pointsToWin - 1f)));
}

また、ゲームの準備段階でラケットの初期サイズが正しくなるように、ラケットが起動したときにスコアをリセットする必要があります。

void Awake ()
{
SetScore(0);
}

AIがボールを打ちやすくするために、下のラケットには少なくとも1、上のラケットには3.5を使用しましょう。

アクティブなカメラ

ゲームプレイが完了したら、ゲームのプレゼンテーションをより面白くできるかどうかを見てみましょう。ボールが何かに当たったときの重い衝撃の感覚を与える簡単な方法は、カメラを振ることです。ボールがアリーナの側面に当たったときにプレーヤーがどのように感じるかをシミュレートします。没入感をさらに高めるために、トップダウン ビューからパース ビューに切り替えます。カメラの位置を (0、20、-19) に設定し、X 回転を 50 に設定します。

プレイヤーの視点からアリーナを表示します

押したり押したり

カメラの動作を制御するには、コンポーネント タイプを作成します。カメラは、特定のパルスで XZ 平面内で動かされるか、Y 次元内で動かされることができます。これは、押し寄せたり押したりするフレームがすべて完了した後に、カメラに 3D スピードを与えることで実現されます。**LivelyCamera**``LateUpdate

プッシュは、構成可能な強度 (デフォルトでは 40 に設定) によって Y 速度を増加させるパブリック メソッドを介して行われます。プッシュは、速度に追加される 2D パルス パラメーターを使用したパブリック メソッドを介して行われ、構成可能なプッシュ強度係数によってスケールされ、デフォルトでは 1 に設定されます。JostleY``PushXZ

using UnityEngine;

public class LivelyCamera : MonoBehaviour
{
[SerializeField, Min(0f)]
float
jostleStrength = 40f,
pushtrength = 1f;

Vector3 velocity;

public void JostleY () => velocity.y += jostleStrength;

public void PushXZ (Vector2 impulse)
{
velocity.x += pushStrength * impulse.x;
velocity.z += pushStrength * impulse.y;
}

void LateUpdate ()
{
transform.localPosition += velocity * Time.deltaTime;
}
}

このコンポーネントをメイン カメラに追加し、構成フィールドを指定してカメラに接続します。**Game**

[SerializeField]
LivelyCamera livelyCamera;

でバウンスが検出された場合、バウンスを実行する前にボールの速度をパルスとして使用して呼び出されます。BounceXIfNeeded``PushXZ

void BounceXIfNeeded (float x)
{
float xExtents = arenaExtents.x - ball.Extents;
if (x < -xExtents)
{
livelyCamera.PushXZ(ball.Velocity);
ball.BounceX(-xExtents);
}
else if (x > xExtents)
{
livelyCamera.PushXZ(ball.Velocity);
ball.BounceX(xExtents);
}
}

Y リターンを実行する前に同じことを実行します。また、得点中にカメラを押します。BounceY

void BounceY (float boundary, Paddle defender, Paddle attacker)
{
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;
livelyCamera.PushXZ(ball.Velocity);
ball.BounceY(boundary);

if (defender.HitBall(bounceX, ball.Extents, out float hitFactor))
{
ball.SetXPositionAndSpeed(bounceX, hitFactor, durationAfterBounce);
}
else
{
livelyCamera.JostleY();
if (attacker.ScorePoint(pointsToWin))
{
EndGame();
}
}
}

スプリングとダンパー

カメラは押し回されますが、その速度は変わらないため、アリーナはすぐに視界から消えます。跳ね返すには、単純なバネ機構を使用して元の位置に保持します。構成可能なスプリングの強さをデフォルト設定の 100、デフォルトの減衰強さ 10 に設定します。また、アンカー位置を与え、起動時のカメラの位置に設定します。**LivelyCamera**

[SerializeField, Min(0f)]
float
springStrength = 100f,
dampingStrength = 10f,
jostleStrength = 40f,
pushStrength = 1f;

Vector3 anchorPosition, velocity;

void Awake () => anchorPosition = transform.localPosition;

カメラの現在の変位を加速度として使用し、バネの強さでスケールしてバネを実装します。また、減衰の強さに比例する電流速度に等しい負の加速度によって動きを遅くします。

void LateUpdate ()
{
Vector3 displacement = anchorPosition - transform.localPosition;
Vector3 acceleration = springStrength * displacement - dampingStrength * velocity;
velocity += acceleration * Time.deltaTime;
transform.localPosition += velocity * Time.deltaTime;
}

高フレームレートの鮮明なカメラ。

最大デルタ時間

私たちの単純なバネの法則は、フレーム レートが十分に高い場合にのみ適切に機能します。カメラを押したり引いたりしてアンカーポイントに戻すことには抵抗しますが、オーバーシュートが発生したり、静止する前に少し揺れたりする可能性があります。ただし、フレーム レートが低すぎると、オーバーシュートによって勢いが誇張され、制御不能になり、減速するどころか加速してしまう可能性があります。この問題は、add メソッドを使用して非常に低いフレーム レートを強制することで確認できます。この設定は永続的なものであるため、制限を削除するには後でゼロに戻す必要があります。Application.targetFrameRate = 5;``Awake

フレーム レートが十分に高い場合、この問題は発生しません。したがって、より小さな時間増分を強制することでこれを回避できます。これはカメラを動かすことで実現できます。ただし、これにより正確な時間増分が強制されるため、カメラがフレームごとに同じ回数更新しない可能性があるため、マイクロスタッタリングが発生します。これは、ビュー全体の動きに影響を与えるため、非常に顕著です。さらに、カメラの動きの実効フレーム レートも制限されます。FixedUpdate

簡単な解決策は、最小時間増分ではなく最大時間増分を強制することです。これに構成可能な最大値を追加します。デフォルトでは 60 分の 1 秒に設定されます。次に、コードをパラメータとして時間デルタを持つ新しいメソッドに移動します。現在のフレームの増分が収まる回数だけ最大増分で呼び出し、残りの増分について再度呼び出します。**LivelyCamera**``LateUpdate``TimeStep``LateUpdate``TimeStep

[SerializeField, Min(0f)]
float
springStrength = 100f,
dampingStrength = 10f,
jostleStrength = 40f,
pushStrength = 1f,
maxDeltaTime = 1f / 60f;

…

void LateUpdate ()
{
float dt = Time.deltaTime;
while (dt > maxDeltaTime)
{
TimeStep(maxDeltaTime);
dt -= maxDeltaTime;
}
TimeStep(dt);
}

void TimeStep (float dt)
{
Vector3 displacement = anchorPosition - transform.localPosition;
Vector3 acceleration = springStrength * displacement - dampingStrength * velocity;
velocity += acceleration * dt;
transform.localPosition += velocity * dt;
}

ビジョン

最後のステップは、ゲームのビジュアルをわずかに改善することです。シンプルな輝くネオンの外観を選択します。

まずはすべてを暗くし、指向性ライトをオフにすることから始めます。また、カメラを単色の背景に設定します。次に、カメラのオプションを有効にして FXAA に設定します。

背景が黒であっても環境照明をそのまま維持するのは意味がありません。そのため、アリーナを視認できるようにするための照明がまだ残っています。

为什么使用 FXAA 而不是 MSAA?
我们将使用高强度HDR颜色。URP的FXAA可以处理这些问题,但其MSAA不能。

光るボール

ボールが唯一の光源になります。サブポイント ライトを与え、色を黄色、強度を 20、範囲を 100 に設定します。

ボールを光源として使用した暗い視覚効果

ボールが光るはずです。HDR カラー プロパティを使用して、アンリット シェーダ マップを作成します。これを使用して、高輝度の黄色のマテリアルを作成し、ボール プレハブに割り当てます。

輝いて見えるようにするには、ブルーム ポスト エフェクトを適用する必要があります。以下の方法でグローバルボリュームを作成します。それを選択し、新しいボリューム プロファイルを作成します。オーバーライドを追加するには、1 に設定して有効にします。さらに、set のオーバーライドが ACES に追加されました。

URP リソースを調整して、HDR に設定します。に設定することもできます。これにより、ボリューム データを変更することがないため、Unity がフレームごとにボリューム データを不必要に更新することがなくなります。

光るボール

粒子をバウンドさせる

ボールは高エネルギーの立方体のように見えます。この考えを強化するために、跳ねるときに火花を散らしてみましょう。頂点カラーに強度属性を乗算した値を使用する、アンリット シェーダ グラフを作成します。サーフェス タイプを透明に、ブレンド モードを加算に設定して、常に明るい状態にします。強度を 10 に設定してパーティクル マテリアルを作成します。

パーティクルのシェーダーグラフ

                                   粒子的着色器图(上图)

原点にパーティクル システム ゲーム オブジェクトを作成します。無効にし、定数範囲 0.5 ~ 1、2 ~ 4、および 0.5 に設定します。とに変更します。

モジュールの両方のレートをゼロに設定します。

デフォルトでは、モジュールは円錐に設定されています。その円錐はそのままにしますが、45 に設定し、0.5 に設定します。

モジュールを有効にすると、色のグラデーションは黄色から赤になり、そのアルファは両端で 0 に設定され、255% で 10% に設定されます。

モジュールを有効にすると、直線曲線は 1 から 0 に減少します。

立方体のみを使用する制約はパーティクルにも適用されるため、デフォルトで立方体を使用するようにモジュールを設定します。当社の粒状材料を使用してみましょう。

このパーティクル システムはボール自体の一部ではありませんが、パーティクルを生成する構成可能な参照と、バウンスごとに生成されるパーティクルの数を制御する構成オプション (デフォルトでは 20 に設定されています) が必要です。**Ball**

[SerializeField]
ParticleSystem bounceParticleSystem;

[SerializeField]
int bounceParticleEmission = 20;

バウンス パーティクル システムを呼び出して、バウンス パーティクルの放出を処理する方法を作成します。放出コーンは正しく配置および回転する必要があるため、X および Z 位置と Y 回転をパ​​ラメータとしてメソッドに指定します。これらを使用して、システムの形状モジュールを調整します。Emit

void EmitBounceParticles (float x, float z, float rotation)
{
ParticleSystem.ShapeModule shape = bounceParticleSystem.shape;
shape.position = new Vector3(x, 0f, z);
shape.rotation = new Vector3(0f, rotation, 0f);
bounceParticleSystem.Emit(bounceParticleEmission);
}

適切なパラメータを指定してこのメ​​ソッドを呼び出します。境界は、対応する次元内の位置です。2 番目の位置は、ボールの位置をリバウンドの瞬間まで反転させることで見つけることができます。回転は、バウンス サイズと、境界が負か正かによって異なります。BounceX``BounceY

public void BounceX (float boundary)
{
float durationAfterBounce = (position.x - boundary) / velocity.x;
position.x = 2f * boundary - position.x;
velocity.x = -velocity.x;
EmitBounceParticles(
boundary,
position.y - velocity.y * durationAfterBounce,
boundary < 0f ? 90f : 270f
);
}

public void BounceY (float boundary)
{
float durationAfterBounce = (position.y - boundary) / velocity.y;
position.y = 2f * boundary - position.y;
velocity.y = -velocity.y;
EmitBounceParticles(
position.x - velocity.x * durationAfterBounce,
boundary,
boundary < 0f ? 0f : 180f
);
}

画像の説明を追加してください

                                      反弹粒子

開始粒子

ゲーム開始時にボールが出現したときに火花を散らしてみましょう。既存のパーティクル システムをプレハブに変換し、新しい開始パーティクル システムの原点にある 2 番目のインスタンスを使用します。持続時間を長くし、球に設定するには、これを 0.5 ~ 1.5 に増やします。

設定フィールドと、開始時にスポーンするパーティクルの数のフィールド (デフォルトでは 100 に設定されています) を追加します。これらのパーティクルは、新しいゲームを開始するときに放出されます。**Ball**

[SerializeField]
ParticleSystem bounceParticleSystem, startParticleSystem;

[SerializeField]
int
bounceParticleEmission = 20,
startParticleEmission = 100;

…

public void StartNewGame ()
{
…
startParticleSystem.Emit(startParticleEmission);
}

開始粒子。

トレイル粒子

3 番目で最後のパーティクル エフェクトは、ボールが残した軌跡になります。パーティクル システム プレハブの別のインスタンスを作成し、今回はそれを使用して有効にします。1 ~ 1.25 に設定し、ゼロに設定します。ボリュームから放射するボックスに変更します。移動時にパーティクルを放出するには、2 に設定します。

また、このシステムの設定フィールドを追加して、視覚化を更新するときにその位置とボールの位置を同期させます。ゲーム終了時にボールが非アクティブ化された後も表示されたままになるように、トレイル システムをボールの子にはしません。そうしないと、航跡はすぐに消えてしまいます。**Ball**

[SerializeField]
ParticleSystem bounceParticleSystem, startParticleSystem, trailParticleSystem;

…

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

画像の説明を追加してください

                                           尾迹粒子

これは、各ゲームの終了時と開始時にボールの移動に合わせて軌道エフェクトも追従することを除いて機能します。2 つのことを行うことでこれを回避できます。まず、ゲームの終了時に起動をオフにし、新しいゲームの開始時に起動をオンにする必要があります。これは、発行モジュールのプロパティを設定することによって行われるため、このための便利なメソッドを追加しましょう。

public void StartNewGame ()
{
…
SetTrailEmission(true);
}

public void EndGame ()
{
…
SetTrailEmission(false);
}

…

void SetTrailEmission (bool enabled)
{
ParticleSystem.EmissionModule emission = trailParticleSystem.emission;
emission.enabled = enabled;
}

第二に、パーティクル システムは古い位置を記憶しています。新しいゲームの開始時にテレポートされた軌跡が表示されないようにクリアするには、それを呼び出す必要があります。Play

SetTrailEmission(true);
trailParticleSystem.Play();

これは、現在明示的に再生しているため、無効に戻すことができることを意味します。

反応面

光を発できるのはボールとその粒子だけではありません。また、その表面を一時的に光らせることで攻撃に反応させましょう。HDR とプロパティを使用してライティング シェーダ マップを作成します。デフォルト値は -1000 です。

発光の色は、最後のヒットがいつ発生したかによって異なります。ヒットするとフル回転し、次の瞬間に直線的にフェードアウトします。これは、現在の時間から最後のヒットの時間を減算し、その時間を 1 から減算し、それを飽和させ、それを使用して発光カラーをスケールすることによって実行できます。

リアクティブ サーフェスのシェーダー グラフ

                                   反应表面的着色器图

このシェーダ マップを使用してマテリアルを作成し、それをシェーディング プレハブで使用します。ベースカラーとして白を使用し、発光カラーとして高輝度の白を使用します。

ウェイクアップ時にマテリアル インスタンスを取得し、成功時に最後のヒット時間を更新します。これにより、ボールを打ったときにラケットが輝きます。**Paddle**``HitBall

static readonly int timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit");

…

Material paddleMaterial;

void Awake ()
{
paddleMaterial = GetComponent<MeshRenderer>().material;
SetScore(0);
}

…

public bool HitBall (float ballX, float ballExtents, out float hitFactor)
{
…

bool success = -1f <= hitFactor && hitFactor <= 1f;
if (success)
{
paddleMaterial.SetFloat(timeOfLastHitId, Time.time);
}
return success;
}

画像の説明を追加してください

                                             反应桨

さらに一歩進めて、相手が得点したときに、相手のゴールとなるアリーナの境界線を輝かせましょう。カラーをミディアム グレーに設定して別の反応性サーフェス マテリアルを作成し、アリーナの境界プレハブに使用します。次に、ターゲットの構成可能なリファレンスと、構成可能な HDR ターゲット カラーを提供します。**Paddle**``MeshRenderer

Racket が起動してマテリアルの発光カラーをターゲット カラーに設定すると、ターゲット マテリアルのインスタンスを取得します。得点時の最後のヒット時間を設定します。

static readonly int
emissionColorId = Shader.PropertyToID("_EmissionColor"),
timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit");

[SerializeField]
TextMeshPro scoreText;

[SerializeField]
MeshRenderer goalRenderer;

[SerializeField, ColorUsage(true, true)]
Color goalColor = Color.white;

…

Material goalMaterial, paddleMaterial;

void Awake ()
{
goalMaterial = goalRenderer.material;
goalMaterial.SetColor(emissionColorId, goalColor);
paddleMaterial = GetComponent<MeshRenderer>().material;
SetScore(0);
}

…

public bool ScorePoint (int pointsToWin)
{
goalMaterial.SetFloat(timeOfLastHitId, Time.time);
SetScore(score + 1, pointsToWin);
return score >= pointsToWin;
}

ピックを対応するレンダーに接続します。下のプレーヤーの色として高輝度の緑を使用し、上の AI の色として高輝度の赤を使用します。

反応的な目標。

色付きのテキスト

最後にテキストに色を付けて光らせます。まず、テキスト プレハブのデフォルトのフォント マテリアルの色を高輝度の黄色に設定します。

光るテキスト

                                        发光的文本

ゴールの色を使用してスコアを表示しますが、ちょっとした工夫が必要です。ゼロから黒で開始するので、最初は黒い背景では端数が表示されません。スコアの色が勝利スコアと同じになると、彼らは最大限の力を発揮します。

この場合、マテリアル インスタンスは、Surface Color シェーダ プロパティという名前が付けられたテキストのプロパティを介して取得されます。fontMaterial

static readonly int
emissionColorId = Shader.PropertyToID("_EmissionColor"),
faceColorId = Shader.PropertyToID("_FaceColor"),
timeOfLastHitId = Shader.PropertyToID("_TimeOfLastHit");

…

Material goalMaterial, paddleMaterial, scoreMaterial;

void Awake ()
{
goalMaterial = goalRenderer.material;
goalMaterial.SetColor(emissionColorId, goalColor);
paddleMaterial = GetComponent<MeshRenderer>().material;
scoreMaterial = scoreText.fontMaterial;
SetScore(0);
}

…

void SetScore (int newScore, float pointsToWin = 1000f)
{
score = newScore;
scoreText.SetText("{0}", newScore);
scoreMaterial.SetColor(faceColorId, goalColor * (newScore / pointsToWin));
SetExtents(Mathf.Lerp(maxExtents, minExtents, newScore / (pointsToWin - 1f)));
}
                                       改变颜色的分数

デフォルト設定では、AI の弱点を発見すると、簡単に AI を倒すことができることに注意してください。これにより開発が促進されますが、望ましいレベルの挑戦を提供するように調整する必要があります。
(終わり)

おすすめ

転載: blog.csdn.net/weixin_72715182/article/details/130482071