1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】物理破壊パズル×ライブ配信シミュレーション「CLASH!LIVE!」の技術解説

1
Posted at

feature_graphic.png
https://play.google.com/store/apps/details?id=com.sst.clash

はじめに

「CLASH!LIVE!」は、物理破壊パズルとライブ配信シミュレーションを掛け合わせたモバイル向けゲームです。プレイヤーはハンマーで建造物を叩き壊しながら、同時にライブ配信でスパチャ(投げ銭)を集めるという、ちょっと変わった体験を目指しました。

本記事では、Unity 6 LTS で開発しているこのゲームの主要な技術的仕組みを紹介します。

ゲーム概要

画面は左右に分割されています。

  • 左半分: 物理破壊フィールド(タップで建物を破壊)
  • 右半分: ライブ配信画面(コメント・スパチャ・リズムレーン)

破壊の迫力がスパチャを呼び、スパチャで得た「Hype」がさらに破壊力を高めるという相互フィードバックがコアループです。


1. 物理破壊システム

1.1 ブロックの活性化 — Kinematic → Dynamic 切り替え

建造物のブロックは最初 Rigidbody2D.bodyType = RigidbodyType2D.Kinematic で固定されています。ハンマーの衝撃波が届いた瞬間に Dynamic へ切り替わり、重力と衝突の影響を受け始めます。

// StructureBlock.cs
public void Activate()
{
    if (activated) return;
    activated = true;
    rb.bodyType = RigidbodyType2D.Dynamic;
    rb.gravityScale = 1f;
}

この「静→動」の遷移が崩壊感を生み出す鍵です。最初からDynamicにしてしまうと建物が自重で崩れてしまいますし、全部Kinematicのままでは物理破壊の面白さがありません。爆発の瞬間だけ物理を有効にすることで、プレイヤーの操作に対する即座のフィードバックと、そこからの連鎖的な崩壊を両立しています。

1.2 タップ爆発 — OverlapCircleAll による範囲攻撃

プレイヤーがフィールドをタップすると、Physics2D.OverlapCircleAll で範囲内の全ブロックを取得し、爆発力を与えます。

// HammerController.cs
void DoHit(Vector2 worldPos)
{
    float hype = liveStream != null ? liveStream.HypeMultiplier : 1f;
    float force = baseForce * hype;  // baseForce = 800

    var hits = Physics2D.OverlapCircleAll(worldPos, explosionRadius); // radius = 1.8
    foreach (var col in hits)
    {
        var block = col.GetComponent<StructureBlock>();
        if (block != null)
        {
            block.Activate();
            block.TakeDamage(force * 0.5f);
        }

        var rb = col.attachedRigidbody;
        if (rb != null)
        {
            Vector2 dir = ((Vector2)col.transform.position - worldPos).normalized;
            float dist = Vector2.Distance(col.transform.position, worldPos);
            float falloff = Mathf.Clamp01(1f - dist / explosionRadius);
            rb.AddForce(dir * force * falloff, ForceMode2D.Impulse);
        }
    }
    
    CameraShake.Instance?.Shake(0.25f, 0.15f);
    ExplosionEffect.Spawn(worldPos, explosionRadius);
}

ポイントは 距離減衰(falloff) です。爆心地に近いブロックほど強い力を受け、遠いブロックは軽く揺れる程度になります。これにより、見た目にも自然な爆発の広がりが表現できます。

1.3 衝突連鎖 — ブロック同士の物理ダメージ

ブロックが吹き飛んで別のブロックにぶつかると、衝突速度に基づくダメージが発生します。

// StructureBlock.cs
void OnCollisionEnter2D(Collision2D collision)
{
    if (!activated) return;
    float impact = collision.relativeVelocity.magnitude;
    if (impact > 8f)
    {
        TakeDamage(impact * 15f);
    }
}

閾値 8f を設けることで、軽い接触ではダメージが発生せず、勢いよくぶつかったときだけ壊れるようになっています。これが連鎖崩壊の気持ちよさにつながります。

1.4 上方ブロックへの伝播

ブロックが破壊されたとき、真上にあるブロックも OverlapBoxAll で検出して活性化します。

// StructureBlock.cs(破壊時)
var above = Physics2D.OverlapBoxAll(
    (Vector2)transform.position + Vector2.up * 0.6f,
    new Vector2(size.x * 0.9f, 0.3f), 0f);
foreach (var col in above)
{
    var b = col.GetComponent<StructureBlock>();
    if (b != null) b.Activate();
}

これにより、土台のブロックを壊すと上の構造物が崩れ落ちるという直感的な物理挙動が実現できます。

1.5 視覚フィードバック

ブロックはHPに応じて色が変化します。

// StructureBlock.cs
Color healthColor = Color.Lerp(
    new Color(0.4f, 0.4f, 0.4f),  // 瀕死: 灰色
    baseColor,                      // 満タン: 元の色
    hp / maxHP
);

さらに被弾時には hitFlashTimer でスプライトを白く光らせ、位置を微小にシェイクさせることで、ダメージの手応えを強調しています。


2. ライブ配信シミュレーション

2.1 スーパーチャット(SC)蓄積と Hype レベル

ライブ配信の盛り上がりは LiveStreamManager が管理しています。スパチャの累計金額に応じて HypeLevel(0〜5)が決まり、これが破壊力の倍率に直結します。

// LiveStreamManager.cs
static readonly long[] HypeThresholds = { 100000, 500000, 1000000, 2000000, 3000000 };
static readonly float[] HypeMultipliers = { 1.0f, 1.3f, 1.6f, 1.9f, 2.2f, 2.5f };

public int HypeLevel
{
    get
    {
        for (int i = HypeThresholds.Length - 1; i >= 0; i--)
            if (totalSuperChat >= HypeThresholds[i]) return i + 1;
        return 0;
    }
}

public float HypeMultiplier => HypeMultipliers[Mathf.Clamp(HypeLevel, 0, 5)];

HypeLevel 0 では倍率 1.0×、最大の Level 5 では 2.5× まで攻撃力が上がります。破壊が派手になる → 配信が盛り上がる → スパチャが増える → さらに強くなる、というポジティブフィードバックループです。

2.2 自動コメント生成

配信画面を賑やかにするため、ステージごとにテーマの異なるダミーコメントを自動生成しています。

// LiveStreamManager.cs
static readonly string[][] StageCommentPools = {
    new[] { "初配信きた!", "がんばれ〜", "わくわく", ... },       // ステージ1
    new[] { "今日も壊すぞー", "解体プロ", "音がいい", ... },       // ステージ2
    new[] { "すご!", "連鎖やば", "天才かよ", "神回", ... },       // ステージ3
    new[] { "伝説の配信者", "もう止まらない", "歴史に残る", ... }, // ステージ4
    new[] { "ラスボスきた", "最終決戦", "泣ける", ... },           // ステージ5
};

さらに、コア破壊やダンス、悪評などのイベント専用コメントプールも用意しており、ゲーム内の出来事に応じてコメントの内容が変わります。配信画面がただの飾りではなく、プレイヤーの行動に反応する仕組みです。

2.3 視聴者数シミュレーション

// LiveStreamManager.cs
void UpdateViewerCount()
{
    int target = baseViewers + HypeLevel * 800 + (int)(totalSuperChat / 50000);
    target += Random.Range(-50, 51);
    viewerCount = (int)Mathf.Lerp(viewerCount, target, Time.deltaTime * 0.5f);
}

視聴者数は HypeLevel とスパチャ累計に基づいて計算され、Lerp でなめらかに変動します。ランダムなブレも加えることで、リアルな配信っぽい揺らぎを表現しています。


3. リズムレーンシステム

右側の配信画面にはリズムゲーム要素があり、タイミングよくタップするとスパチャ報酬が得られます。

3.1 フレーズ構造による緩急

ノーツの密度を一定にすると単調になるため、フレーズ(Phrase) という概念で緩急をつけています。

enum PhraseType { Active, Rest, Buildup, Climax }
  • Rest: ノーツが少ない休憩区間
  • Active: 通常の密度
  • Buildup: 徐々にノーツが増える(1小節目は1ノーツ、4小節目は4ノーツ)
  • Climax: 高密度+高倍率ノーツ多め

フレーズの選択確率は HypeLevel に連動 しています。Hype が低いうちは Rest が多く、高くなるとClimax の頻度が上がります。

3.2 BPM の動的変化

currentBPM = baseBPM + hype * 12f;  // baseBPM = 130

HypeLevel が上がるとBPMも加速します。Hype 0 で 130 BPM、Hype 5 で 190 BPM。盛り上がりに合わせてゲームテンポ自体が変わる設計です。

3.3 判定とコンボ

const float BuzzRadius = 15f;   // 最高判定
const float HotRadius = 30f;    // 良判定
const float HitRadius = 50f;    // 可判定

int reward = (int)(baseReward * noteMultiplier * (1 + combo / 5) * bonusMult);

判定は3段階(BUZZ / HOT / NICE)で、コンボが積み重なるほど報酬が増えるシンプルな仕組みです。リズムゲームはあくまでサブシステムなので、複雑にしすぎないことを意識しました。


4. AI コアとシールドシステム

各ステージには破壊目標となる TargetAICore が配置されています。

4.1 HypeLevel ゲートによるシールド

コアは coreShieldLevel を持ち、現在の HypeLevel がそれ以上にならないとシールドが解除されません。

// TargetAICore.cs
public bool IsShielded =>
    shieldLevel > 0 &&
    (LiveStreamManager.Instance == null ||
     LiveStreamManager.Instance.HypeLevel < shieldLevel);

シールド中はダメージが通らないため、プレイヤーは周囲のブロックを壊してスパチャを稼ぎ、Hype を上げてからコアに挑む必要があります。これが 「まず配信を盛り上げろ」 というゲームデザインの核心部分です。

4.2 コア破壊演出

コアのHPがゼロになると、派手な爆発演出が走ります。

// TargetAICore.cs
void Explode()
{
    float force = 1500f;
    float radius = 8f;
    var hits = Physics2D.OverlapCircleAll(transform.position, radius);
    foreach (var col in hits)
    {
        var rb = col.attachedRigidbody;
        if (rb != null)
        {
            Vector2 dir = ((Vector2)col.transform.position - (Vector2)transform.position).normalized;
            rb.AddForce(dir * force, ForceMode2D.Impulse);
        }
    }
    // 12個のデブリを放射状に生成
    for (int i = 0; i < 12; i++)
    {
        float angle = i * (360f / 12) * Mathf.Deg2Rad;
        // ... デブリ生成処理
    }
}

通常のハンマー爆発(force 800, radius 1.8)に対して、コア爆発は force 1500, radius 8 と桁違いの規模です。周囲のブロックが一斉に吹き飛ぶカタルシスを演出しています。


5. ScriptableObject によるレベルデータ

5.1 LevelData の構造

ステージデータは ScriptableObject で定義しています。

// LevelData.cs
[CreateAssetMenu(fileName = "Level", menuName = "CrashLive/LevelData")]
public class LevelData : ScriptableObject
{
    public string levelID;
    public int allowedTaps;         // タップ回数制限
    public int targetCoreCount;     // 破壊目標コア数
    public long requiredSuperChat;  // SC ノルマ
    public int coreShieldLevel;     // シールド解除に必要な Hype
    public List<BlockData> blocks;  // ブロック配置データ
}

[System.Serializable]
public class BlockData
{
    public string prefabName;
    public Vector2 position;
    public float rotationZ;
}

allowedTaps(タップ回数制限)と requiredSuperChat(SCノルマ)の2つのクリア条件が緊張感を生みます。限られたタップで効率よく壊しつつ、配信の盛り上がりも維持しなければならないというジレンマです。

5.2 プレハブキャッシュによるロード最適化

// LevelManager.cs
Dictionary<string, GameObject> prefabCache = new Dictionary<string, GameObject>();

GameObject GetPrefab(string name)
{
    if (!prefabCache.TryGetValue(name, out var prefab))
    {
        prefab = Resources.Load<GameObject>($"Prefabs/{name}");
        prefabCache[name] = prefab;
    }
    return prefab;
}

同じプレハブ名は辞書でキャッシュし、Resources.Load の呼び出し回数を削減しています。ステージ内に同種のブロックが大量に配置される場面では効果的です。


6. 演出系の工夫

6.1 プロシージャル爆発エフェクト

爆発エフェクトは画像素材を使わず、コードで動的に生成しています。

// ExplosionEffect.cs(抜粋)
float innerRadius = progress * center;
float outerRadius = Mathf.Min(center, innerRadius + center * 0.4f);

if (t < 0.3f)
    c = new Color(1f, 1f, 0.8f, alpha);      // 中心: 白〜黄
else if (t < 0.6f)
    c = new Color(1f, 0.6f, 0.1f, alpha);    // 中間: オレンジ
else
    c = new Color(1f, 0.2f, 0f, alpha * (1f - t)); // 外縁: 赤→透明

8フレームのスプライトを Texture2D で生成し、拡大するリング状のグラデーションで爆発を表現しています。Time.unscaledDeltaTime を使っているので、スローモーション演出中でもエフェクトは通常速度で再生されます。

6.2 カメラシェイク

// CameraShake.cs
public void Shake(float duration, float intensity)
{
    shakeDuration = duration;
    shakeIntensity = intensity;
}

void LateUpdate()
{
    if (shakeDuration > 0f)
    {
        float t = shakeDuration * shakeIntensity;
        float x = Random.Range(-t, t);
        float y = Random.Range(-t, t);
        transform.localPosition = originalPos + new Vector3(x, y, 0);
        shakeDuration -= Time.unscaledDeltaTime;
    }
}

揺れの大きさが shakeDuration * shakeIntensity なので、時間経過とともに自然に減衰します。別途イージング関数を用意しなくても、durationの減少がそのまま減衰カーブになるシンプルな設計です。

6.3 ドローンカメラ

配信画面の雰囲気を出すため、フィールド上空を飛ぶドローンカメラのオブジェクトを配置しています。

// DroneCameraAI.cs
float bob = Mathf.Sin(Time.time * bobSpeed + bobPhase) * bobAmplitude;
transform.position = basePos + new Vector3(0, bob, 0);

正弦波による上下の揺らぎ(bobbing)と、ランダムな目標地点へのスムーズな移動で、自律的に飛行している感じを出しています。RECランプの点滅もついており、「撮影されている」感を演出しています。


7. ステート管理とクリア判定

7.1 GameManager のステートマシン

// GameManager.cs
public enum GameState
{
    Title, StageSelect, Playing, Paused,
    StageClear, GameOver, Ending, Tutorial
}

ステート遷移は static event Action<GameState> OnStateChanged で通知され、各コンポーネントがそれを購読する形です。

// CameraShake.cs での利用例
void OnEnable() => GameManager.OnStateChanged += OnStateChanged;

void OnStateChanged(GameManager.GameState state)
{
    if (state == GameManager.GameState.Playing)
        cam.rect = new Rect(0f, 0f, 0.5f, 1f);  // 左半分のみ
    else
        cam.rect = new Rect(0f, 0f, 1f, 1f);     // 全画面
}

7.2 クリア条件の二重チェック

ステージクリアには コア全破壊 と SCノルマ達成 の両方が必要です。

// LevelManager.cs
public bool IsClearConditionMet()
{
    if (currentLevel == null) return false;
    bool coresCleared = destroyedCoreCount >= currentLevel.targetCoreCount;
    bool scMet = currentLevel.requiredSuperChat <= 0 ||
        (LiveStreamManager.Instance != null &&
         LiveStreamManager.Instance.TotalSuperChat >= currentLevel.requiredSuperChat);
    return coresCleared && scMet;
}

コアを全部壊してもSCが足りなければクリアにならないため、プレイヤーは破壊と配信の両方に意識を配る必要があります。

7.3 コア破壊のスローモーション演出

コア破壊の瞬間には Time.timeScale を落としてスローモーション演出を入れています。

// GameManager.cs
IEnumerator CoreDestructionSequence()
{
    Time.timeScale = 0.3f;
    CameraShake.Instance?.Shake(0.5f, 0.3f);
    yield return new WaitForSecondsRealtime(1.5f);
    Time.timeScale = 1f;
}

WaitForSecondsRealtime を使うことで、timeScale に影響されず正確な待機時間を確保しています。


8. ランクシステム

プレイヤーの進行は E → D → C → B → A → S の6段階ランクで管理されます。各ランク内は5ステージ構成で、計30ステージです。

// GameManager.cs
static readonly string[] RankNames = { "E", "D", "C", "B", "A", "S" };
int currentRank = 0;   // 0=E, 5=S
int currentStage = 0;  // 0〜4

ランクが上がるほど、コアのシールドレベルが高くなり、タップ数制限が厳しくなり、SCノルマも増えていきます。データは全て LevelData の ScriptableObject で定義されているので、バランス調整はInspector上で行えます。


まとめ

「CLASH!LIVE!」の技術的なポイントをまとめると:

システム 要点
物理破壊 Kinematic→Dynamic 切替で「崩壊の瞬間」を作る
爆発 OverlapCircleAll + 距離減衰で自然な爆風
連鎖 衝突速度ベースのダメージ + 上方伝播
配信シミュ SC蓄積 → HypeLevel → 破壊力倍率のフィードバックループ
リズムレーン フレーズ構造による緩急 + HypeLevel連動BPM
シールド HypeLevel ゲートで「まず盛り上げろ」を強制
レベル設計 ScriptableObject で全パラメータを外部化
演出 プロシージャル爆発 + 自然減衰カメラシェイク

2つの異なるジャンル(物理パズルとライブ配信)を HypeLevel という1つの数値 で繋いだことが、このゲームの設計上の肝でした。物理破壊とスパチャという一見無関係な要素が、HypeLevel を介して互いに影響し合うことで、どちらか一方だけでは成立しないゲーム体験になっています。

個人開発の参考になれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?