見出し画像

unity1week「ない」参加作品『サキュバストーコの暴走キックボード』のスクリプト公開【途中まで無料】

この記事ではunity1week「ない」の参加作品『サキュバストーコの暴走キックボード』で作ったスクリプトを公開します。途中から有料といたします。

↓備忘録は全文無料です!

今回のスクリプトは基本的にChatGPTo1を使って作りました。ChatGPTo1は本当に優秀過ぎるので、是非使ってみてください。


1.CameraFollow

プレイヤー(電動キックボード)を追従するカメラです。
カメラを電動キックボードの子オブジェクトにし、そのカメラにアタッチします。
特徴としてはこのゲームのカメラは左右に移動しません(X座標固定)。
FollowSpeedは速くても追い越すことはないので、速いほうが良いです。遅いとプレイヤーのスピードが速くなった時にカメラとプレイヤーに距離ができてしまいます。

using UnityEngine;

public class CameraFollow : MonoBehaviour
{
    public Transform player;

    [Header("Camera Offset Settings")]
    [Tooltip("プレイヤーからの相対位置を維持したい場合、このオフセットを使用")]
    public Vector3 offset = new Vector3(0, 5, -10);

    [Header("Follow Speed Settings")]
    [Tooltip("カメラがプレイヤーとの一定距離を保つための補間速度")]
    public float followSpeed = 5f;

    [Header("X座標固定")]
    [Tooltip("カメラのX座標を固定したい場合はtrue")]
    public bool fixXPosition = true;
    public float fixedX = 0f;

    private Vector3 initialOffset;      // 実行時に算出したプレイヤーとの相対オフセット
    private Quaternion initialRotation; // カメラの初期回転

    void Start()
    {
        // カメラの初期回転を記憶
        initialRotation = transform.rotation;

        // プレイヤーが存在すれば、実行時の相対座標を初期オフセットとして保存
        if (player != null)
        {
            // もしoffsetの値を使わず、シーン上のカメラ-プレイヤーの実距離を初期値にしたいなら:
            // initialOffset = transform.position - player.position;
            // 今回はpublicなoffset変数をそのまま使用します:
            initialOffset = offset;
        }
    }

    void LateUpdate()
    {
        if (player == null) return;

        // 目標位置 = プレイヤー位置 + (初期オフセット)
        Vector3 targetPos = player.position + initialOffset;

        // X座標を固定したいなら適用
        if (fixXPosition)
        {
            targetPos.x = fixedX;
        }

        // カメラを滑らかに目標位置へ移動 (一定距離を保つ)
        transform.position = Vector3.Lerp(transform.position, targetPos, followSpeed * Time.deltaTime);

        // カメラの回転は初期回転を維持
        transform.rotation = initialRotation;
    }
}

2.EnemySpawner

障害物である車をオブジェクトプールで管理し、一定間隔で表示させるスクリプトです。scooterの走行距離によって表示間隔が短くなり、二台同時に表示する確率が高くなります。
また車の表示位置は、車線の真ん中からLaneOffset分左右に幅があります。

using UnityEngine;
using Cysharp.Threading.Tasks;      
using Sirenix.OdinInspector;        
using Sirenix.Serialization;        
using System.Collections.Generic;
using System;

public class EnemySpawner : SerializedMonoBehaviour
{
    [Title("プレイヤー参照")]
    [SerializeField] private Transform player;   
    [SerializeField] private ScooterController scooterController;

    [Title("敵Prefab関連")]
    [Tooltip("EnemyCarフォルダ内のPrefabを登録しておく")]
    [SerializeField] private GameObject[] enemyPrefabs; 

    [Title("Spawn間隔設定")]
    [Tooltip("ベースとなるSpawn間隔(秒)")]
    [SerializeField] private float baseSpawnInterval = 2f; 

    [Tooltip("最短Spawn間隔(秒)")]
    [SerializeField] private float minSpawnInterval = 0.5f; 

    [Tooltip("プレイヤー速度とSpawn間隔の関係を決める係数")]
    [SerializeField] private float intervalSpeedFactor = 0.1f;  

    [Title("2台同時生成の確率設定")]
    [Tooltip("速度が0のときの2台生成確率")]
    [SerializeField, Range(0f, 1f)] 
    private float baseTwoSpawnProbability = 0.2f; 

    [Tooltip("最大の2台生成確率(速度が最大近くなった時)")]
    [SerializeField, Range(0f, 1f)] 
    private float maxTwoSpawnProbability = 0.8f; 

    [Tooltip("速度と2台生成確率を結びつける係数")]
    [SerializeField] private float twoSpawnSpeedFactor = 0.02f;  

    [Title("車線関連設定")]
    [Tooltip("車線のX座標候補 (±laneOffset内でブレ)")]
    [SerializeField] private float[] laneXPositions = new float[] { -3f, 0f, 3f };
    [SerializeField] private float laneOffset = 0.2f; 

    [Title("Z座標オフセット")]
    [Tooltip("プレイヤーZ + spawnZBase に ±spawnZRangeでブレ")]
    [SerializeField] private float spawnZBase = 200f;
    [SerializeField] private float spawnZRange = 5f;

    [Title("後方で非表示にする距離")]
    [Tooltip("プレイヤーZ - disappearDistanceを下回ったら非表示")]
    [SerializeField] private float disappearDistance = 50f;

    [Title("その他ルール")]
    [Tooltip("1回の生成で最大何台出すか (1 or 2 を想定)")]
    [SerializeField] private int maxSpawnCountPerTime = 2;

    private bool _isSpawning;
    private int _lastPrefabIndex = -1; 
    private List<GameObject> _activeEnemies = new List<GameObject>();

    [Button("スポーン開始")]
    private void StartSpawningButton()
    {
        if (!_isSpawning)
        {
            _isSpawning = true;
            SpawnLoopAsync().Forget(); 
        }
    }

    [Button("スポーン停止")]
    public void StopSpawningButton()
    {
        _isSpawning = false;
    }

    private void Start()
    {
        // 自動でスポーン開始
        if (!_isSpawning)
        {
            _isSpawning = true;
            SpawnLoopAsync().Forget();
        }
    }

    private void Update()
    {
        DespawnPassedEnemies();
    }

    /// <summary>
    /// スポーンを非同期ループで行う (UniTask)
    /// </summary>
    private async UniTaskVoid SpawnLoopAsync()
    {
        while (_isSpawning)
        {
            // 1) ScooterControllerから速度を取得
            float scooterSpeed = (scooterController != null) ? scooterController.CurrentSpeed : 0f;

            // 2) spawnIntervalを計算(速度が上がるほど短く)
            float spawnInterval = Mathf.Max(
                minSpawnInterval,
                baseSpawnInterval - scooterSpeed * intervalSpeedFactor
            );

            // 3) 2台同時生成の確率
            float twoSpawnProb = Mathf.Min(
                maxTwoSpawnProbability,
                baseTwoSpawnProbability + scooterSpeed * twoSpawnSpeedFactor
            );

            // 4) 1 or 2台生成を判定
            bool willSpawnTwo = false;
            if (maxSpawnCountPerTime >= 2)
            {
                float rand = UnityEngine.Random.value; 
                willSpawnTwo = (rand < twoSpawnProb);
            }

            if (willSpawnTwo)
                SpawnTwoEnemies();
            else
                SpawnOneEnemy();

            // 次の生成まで待機
            await UniTask.Delay(TimeSpan.FromSeconds(spawnInterval));
        }
    }

    private void SpawnOneEnemy()
    {
        int prefabIndex = GetRandomPrefabIndexAvoidSame(_lastPrefabIndex);

        float xPos = GetRandomLaneX();
        float zPos = player.position.z + spawnZBase + UnityEngine.Random.Range(-spawnZRange, spawnZRange);

        float prefabY = enemyPrefabs[prefabIndex].transform.position.y;
        Quaternion rot = enemyPrefabs[prefabIndex].transform.rotation;

        GameObject enemy = Instantiate(enemyPrefabs[prefabIndex],
            new Vector3(xPos, prefabY, zPos),
            rot);

        _activeEnemies.Add(enemy);
        _lastPrefabIndex = prefabIndex;
    }

    private void SpawnTwoEnemies()
    {
        int firstPrefabIndex = GetRandomPrefabIndexAvoidSame(_lastPrefabIndex);
        int secondPrefabIndex;
        do
        {
            secondPrefabIndex = GetRandomPrefabIndexAvoidSame(_lastPrefabIndex);
        } while (secondPrefabIndex == firstPrefabIndex);

        int[] laneIndices = GetTwoDistinctLaneIndices();
        float xPos1 = GetRandomLaneX(laneIndices[0]);
        float xPos2 = GetRandomLaneX(laneIndices[1]);

        float zBaseVal = player.position.z + spawnZBase;
        float zPos1 = zBaseVal + UnityEngine.Random.Range(-spawnZRange, spawnZRange);
        float zPos2 = zBaseVal + UnityEngine.Random.Range(-spawnZRange, spawnZRange);

        float firstPrefabY = enemyPrefabs[firstPrefabIndex].transform.position.y;
        float secondPrefabY = enemyPrefabs[secondPrefabIndex].transform.position.y;

        Quaternion firstRot = enemyPrefabs[firstPrefabIndex].transform.rotation;
        Quaternion secondRot = enemyPrefabs[secondPrefabIndex].transform.rotation;

        GameObject enemyA = Instantiate(enemyPrefabs[firstPrefabIndex],
            new Vector3(xPos1, firstPrefabY, zPos1),
            firstRot);

        GameObject enemyB = Instantiate(enemyPrefabs[secondPrefabIndex],
            new Vector3(xPos2, secondPrefabY, zPos2),
            secondRot);

        _activeEnemies.Add(enemyA);
        _activeEnemies.Add(enemyB);

        _lastPrefabIndex = secondPrefabIndex;
    }

    private void DespawnPassedEnemies()
    {
        float limitZ = player.position.z - disappearDistance;
        for (int i = 0; i < _activeEnemies.Count; i++)
        {
            var obj = _activeEnemies[i];
            if (obj != null && obj.activeInHierarchy)
            {
                if (obj.transform.position.z < limitZ)
                {
                    obj.SetActive(false);
                }
            }
        }
    }

    private int GetRandomPrefabIndexAvoidSame(int avoidIndex)
    {
        if (enemyPrefabs.Length <= 1) return 0;

        int idx;
        do
        {
            idx = UnityEngine.Random.Range(0, enemyPrefabs.Length);
        } while (idx == avoidIndex);

        return idx;
    }

    private float GetRandomLaneX()
    {
        int laneIndex = UnityEngine.Random.Range(0, laneXPositions.Length);
        return GetRandomLaneX(laneIndex);
    }

    private float GetRandomLaneX(int laneIndex)
    {
        float baseX = laneXPositions[laneIndex];
        float offset = UnityEngine.Random.Range(-laneOffset, laneOffset);
        return baseX + offset;
    }

    private int[] GetTwoDistinctLaneIndices()
    {
        if (laneXPositions.Length < 2) return new int[] { 0, 0 };
        int first = UnityEngine.Random.Range(0, laneXPositions.Length);
        int second;
        do
        {
            second = UnityEngine.Random.Range(0, laneXPositions.Length);
        } while (second == first);
        return new int[] { first, second };
    }
    
    // EnemySpawner.cs などで
    public void StopAllEnemies()
    {
        foreach (var enemy in _activeEnemies)
        {
            if (enemy != null)
            {
                var carCtrl = enemy.GetComponent<EnemyCarController>();
                if (carCtrl != null)
                    carCtrl.StopCar();
            }
        }
    }
    
    //車にGroundタグを付ける。ラグドールが車の上に落ちてしまったときにタイムラインが再生されなかったため
    public void ChangeTagAllEnemies()
    {
        foreach (var enemy in _activeEnemies)
        {
            if (enemy != null)
            {
                enemy.tag = "Ground";
            }
        }
    }
}

3.EnemyCarController 

using UnityEngine;

public class EnemyCarController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 2f; // インスペクタから設定
    private bool stopMoving = false;

    void FixedUpdate()
    {
        if (stopMoving) return;

        // Z軸方向へ低速で前進
        transform.Translate(Vector3.forward * (moveSpeed * Time.fixedDeltaTime));
    }

    public void StopCar()
    {
        stopMoving = true;
    }
}

4.ScooterContorller

電動キックボードの3Dモデルにアタッチすることで、前進させることができます。
速度や走行距離の表示管理やサウンドの再生、ゲームオーバー時の処理も行っています。
50m走行地点をスタート地点にしているのは、そこまでにカウントダウンのタイムラインを再生しているからです。

ここから先は

19,758字 / 4画像

¥ 100

期間限定!Amazon Payで支払うと抽選で
Amazonギフトカード5,000円分が当たる

この記事が気に入ったらチップで応援してみませんか?