目指せUnityマスター【Unity1週間ゲームジャム②】
Q.何この記事?
A.新卒Webエンジニアが、Unityマスターになるまでの過程を描いていく記事。
Unity1週間ゲームジャム参加レポート
今回の目玉は...記事が┗(;´Д`)┛超おもてぇ
今週やったこと
参加レポートサボってたので先々週やったこと...。
unityroomの4~7日目レポート
▼URL
目次
4日目
あんまり覚えてない
地形の生成
このあたりで地形の生成とかしてたと思う。
方法としては、継ぎ目が無いように1000mごとにTerrainを生成するって感じ。
毎フレーム判定するのも何なので、少し前に教わったIEnumeratorを使った最適化をしてみた。
IEnumerator CreateTerrain() { float moveLength = 0; while (true) { moveLength = player.transform.position.z - startPositionZ; //terrainの生成を、1000移動するごとに行う if (moveLength > terrainSpanLengthZ) { prevTerrain = currentTerrain; currentTerrain = Instantiate(terrainPrefab, prevTerrain.transform.position + terrainSpanVector, prevTerrain.transform.rotation); moveLength = 0; startPositionZ = prevTerrain.transform.position.z-terrainSpanLengthZ/2; } yield return new WaitForSeconds(1f); } }
シーン遷移
戦闘シーンとアップグレードシーンを遷移させる処理もこの日だったと思う。
参考↓
↓のコードみたいな感じで、
①引数を(Scene nextScene, LoadSceneMode mode)とする、シーン遷移後の処理をするメソッドを記述
②SceneManager.sceneLoaded(デリゲート)に①の処理を代入(加算)して、SceneManager.LoadScene("ロードするシーン名")
③ボタンに②を記述したメソッドを、クリック時に発火するイベントとして登録
という風にした
void Start() { sceneChangeToFactoryButton = GameObject.FindWithTag("SceneChangeButton").GetComponent<Button>(); sceneChangeToFactoryButton.onClick.AddListener(ChangeSceneToFactory); } void ChangeSceneToFactory() { SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.LoadScene("Factory"); } void OnSceneLoaded(Scene nextScene, LoadSceneMode mode) { FactorySceneController factorySceneController = GameObject.FindWithTag("FactoryGameManager").GetComponent<FactorySceneController>(); factorySceneController.Money = this.Money; factorySceneController.MaxStock = this.MaxStock; factorySceneController.ShootSpan = this.ShootSpan; factorySceneController.LockOnSpan = this.LockOnSpan; factorySceneController.ForceMagnitude = this.ForceMagnitude; factorySceneController.AttackPoint = this.AttackPoint; factorySceneController.SpawnSpan = this.SpawnSpan; factorySceneController.LockOnRadius = this.LockOnRadius; factorySceneController.LaserDamage = this.laserDamage; factorySceneController.SupportCharacter = this.supportCharacter; factorySceneController.LaserCount = this.laserCount; SceneManager.sceneLoaded -= OnSceneLoaded; }
変数が多くて変更に強いとは言えないから、こいつらが宣言してあるところを参照する配列とか、もしくは変更されたらバインディングする関数とか設けておいて、foreach当たりで回せるようにするのがいいかも。
でもまぁここからほかの場所に引っ張ってくる感じにできたので、まぁ悪くはないと思う。
5日目
ここも正直あんまり....というか全然覚えてない。
敵スポーンロジック
この辺はコード見てもらったほうが早いかな?
大体3工程ぐらいにわかれていて、
①ヒエラルキーで敵(または敵グループ)のプレハブを登録
ー他に、各々がどれくらいの確率で出現するか設定したり、どれくらいの場所で、どれくらい範囲がばらつくか設定した。
②①で設定した変数をもとに、”ランダムに出現する敵を設定””ランダムに出現場所を設定”するメソッドを記述
③コルーチンでランダム時間ごとに敵を生成するよう記述
という感じにしてみた。
public class EnemySpawner : MonoBehaviour { [SerializeField] List<GameObject> enemyPrefabs; [SerializeField] List<float> spawnRates = new List<float>(); [SerializeField] float offsetPositionY; float span = 5.0f; [SerializeField] float BoundZ; [SerializeField] float spawnMagnification=1.0f; [SerializeField] float randomY=0f; [SerializeField] float randomX = 0f; [SerializeField] GameObject parent; public float SpawnSpan { set { this.span = value; } get { return this.span; } } //ステータス引継ぎ用コントローラ BattleSceneController battleSceneController; // Start is called before the first frame update void Start() { battleSceneController = GameObject.FindWithTag("GameManager").GetComponent<BattleSceneController>(); InitStatus(); StartCoroutine(InvokeSpawnEnemy()); } void SpawnEnemy() { GameObject enemy = Instantiate(enemyPrefabs[CalcRandomSpawnIndex()],CalcSpawnPosition(),transform.rotation); if (parent) { enemy.transform.SetParent(parent.transform); } } Vector3 CalcSpawnPosition() { Vector3 spawnPosition = new Vector3(Random.Range(transform.position.x - this.randomX, transform.position.x + this.randomX), Random.Range((transform.position.y + offsetPositionY) - this.randomY, (transform.position.y + offsetPositionY) + this.randomY), Random.Range(transform.position.z - BoundZ, transform.position.z + BoundZ)); return spawnPosition; } IEnumerator InvokeSpawnEnemy() { while (true) { float spanValue = span / this.spawnMagnification; yield return new WaitForSeconds(Random.Range(spanValue * 0.7f, spanValue * 1.3f)); SpawnEnemy(); } } void InitStatus() { this.SpawnSpan = battleSceneController.SpawnSpan; } int CalcRandomSpawnIndex() { if(enemyPrefabs.Count == spawnRates.Count) { List<float> rates = CreateSpawnRates(this.spawnRates); float randomValue = Random.Range(0.0f, 1.0f); float rateSum=0.0f; for (var i = 0; i < rates.Count; i++) { rateSum += rates[i]; if(rateSum > randomValue) { return i; } } } Debug.LogWarning("Please set spawnRates " + enemyPrefabs.Count + " rows"); return Random.Range((int)0,(int)enemyPrefabs.Count); } List<float> CreateSpawnRates(List<float> spawnRates) { float sum = 0; List<float> rates = new List<float>(); //割合の合計(1)を出す foreach (float rate in spawnRates) { sum += rate; } //それぞれの割合を出す for (var i = 0; i < spawnRates.Count; i++) { rates.Add(spawnRates[i] / sum); } return rates; } }
デカいロボットにHPバーを追従させる
うかつに敵を追加しようとしたところ、これが意外と厄介だった。
なぜ厄介だったかというと、アニメーションが「モデルを移動させるタイプ」で「ゲームオブジェクトは動かないけど見かけ上のモデルは動いていて、一定間隔ごとに瞬間移動させることで歩いているように見せかける」タイプのものだったからだ。
このタイプになると、HPバーが一定時間ごとにカクカクと追従していって、違和感バリバリなのである。
だから少しでもそれを解消するために、移動先を予測してなめらかに移動するようなメソッドを記述した。
あまりに汚い....。
行っていることとしては簡単で、
①予め前のポジションから次のポジションまでスムーズに移動するメソッドを記述しておく
②ゲームオブジェクトの移動を検知して①を発火させる。
とこのような感じ。移動の検知とオフセットとなるポジション/間隔の記述に時間がかかったと思う。
public class FollowParentSmooth : MonoBehaviour { Vector3 prevPosition; bool isMove=false; float t = 0; Vector3 currentPosition; [SerializeField] float startDelay; [SerializeField] GameObject parent; [SerializeField] Vector3 offsetPosition; float startTime; float durationTime=1.61f; [SerializeField] Vector3 nextMoveOffset; bool isStart = true; // Start is called before the first frame update void Start() { currentPosition = parent.transform.position; prevPosition = currentPosition; StartCoroutine(StartDelay()); } // Update is called once per frame void Update() { StartUpdate(); SwitchDurationTIme(); ScanPosition(); UpdatePosition(); } void ScanPosition() { if(parent.transform.position != prevPosition) { isStart = true; isMove = true; currentPosition = parent.transform.position; //Debug.Log(currentPosition); } } void SwitchDurationTIme() { if (parent.transform.position != currentPosition) { durationTime = Time.time - startTime; startTime = Time.time; //Debug.Log(durationTime); } } void UpdatePosition() { if (isMove) { //transform.position = new Vector3(Mathf.SmoothStep(prevPosition.x, currentPosition.x, t), Mathf.SmoothStep(prevPosition.y, currentPosition.y, t), Mathf.SmoothStep(prevPosition.z, currentPosition.z, t)); transform.position = new Vector3(Smooth(prevPosition.x, currentPosition.x, t), Smooth(prevPosition.y, currentPosition.y, t), Smooth(prevPosition.z, currentPosition.z, t)) + offsetPosition; t += Time.deltaTime/durationTime; if (t>=0.95) { t = 0; isMove = false; prevPosition = currentPosition; } } } float Smooth(float from, float to, float t) { if (t <= 1) { return from + (to - from) * t; } else { return from + (to - from); } } IEnumerator StartDelay() { yield return new WaitForSeconds(startDelay); startTime = Time.time; //currentPosition += nextMoveOffset; isStart = false; } void StartUpdate() { if (isStart == false) { transform.Translate(nextMoveOffset * (Time.deltaTime / durationTime)); } } public Vector3 GetNextMoveOffset() { return this.nextMoveOffset; } }
6日目
覚えてな(ry
アップグレードシーン+UI記述
先に楽なUIのほうから。内部データをUIとして違和感なく表示させているだけ。
public class FactoryUIController : MonoBehaviour { FactorySceneController sceneController; Dictionary<string, TextMeshProUGUI> UIObjects = new Dictionary<string, TextMeshProUGUI>(); // Start is called before the first frame update void Start() { sceneController = GetComponent<FactorySceneController>(); SetUIObjects(); UpdateUI(); } public void UpdateUI() { SetMoneyText(); SetMaxStockText(); SetShootSpanText(); SetLockOnSpanText(); SetLockOnRadiusText(); SetForceMagnitudeText(); SetAttackPointText(); SetSpawnSpanText(); SetLaserDamageText(); SetSupportCharacterText(); SetLaserCountText(); } void SetUIObjects() { GameObject[] UIs = GameObject.FindGameObjectsWithTag("UI"); foreach(GameObject UIObject in UIs) { UIObjects[UIObject.name] = UIObject.GetComponent<TextMeshProUGUI>(); } } void SetMoneyText() { UIObjects["Money"].text = "$"+sceneController.Money; } void SetMaxStockText() { SetText("MaxStock", "発射数", " 発", sceneController.MaxStock); } void SetShootSpanText() { SetText("ShootSpan", "発射レート", " /秒", 1/sceneController.ShootSpan); } void SetLockOnSpanText() { SetText("LockOnSpan", "ロックオン速度", " /秒", 1/sceneController.LockOnSpan); } void SetLockOnRadiusText() { SetText("LockOnRadius", "ロックオン半径", " m", (int)sceneController.LockOnRadius); } void SetForceMagnitudeText() { SetText("ForceMagnitude", "弾速", "m/s",(int)(sceneController.ForceMagnitude/100)); } void SetAttackPointText() { SetText("AttackPoint", "威力", " J",(int)sceneController.AttackPoint); } void SetSpawnSpanText() { SetText("SpawnSpan", "敵の数", " 倍", 5/sceneController.SpawnSpan); } void SetLaserDamageText() { SetText("LaserDamage", "レーザー威力", " J/s", sceneController.LaserDamage * 80); } void SetSupportCharacterText() { SetText("SupportCharacter", "支援ヘリ数", " 体", sceneController.SupportCharacter); } void SetLaserCountText() { SetText("LaserCount", "レーザー数", " 本", sceneController.LaserCount); } void SetText(string key, string name, string unit, int value) { UIObjects[key].text = $"{name}:{value,-6}{unit}"; } void SetText(string key, string name, string unit, float value) { UIObjects[key].text = $"{name}:{value,-6:F2}{unit}"; } void SetText(string key, string name, string unit, double value) { UIObjects[key].text = $"{name}:{value,-6}{unit}"; } }
次にアップグレード処理とボタンの対応付けの記述....。
”と”という言葉があるように、役割を2つ設けてしまってかなりふとっちょになってしまった。
1要素追加しただけで4,5か所ほどに追記しないといけないという頭の悪いコード。
多分、「アップグレード操作」「アップグレードコスト」「UI反映」とかで分けて、それぞれの要素を(多少処理が重くなってもよいので)汎用的にさせられれば良かったかなと思う。
具体的な改善案は、「1要素をアップグレードする処理」のメソッド群と「それぞれのメソッド群を指定して実行する」メソッドで分けられてるから、アップグレードやダウングレードのメソッドをインタフェースかなんかで同じメソッド名で実行できるようにして、アップグレード処理を行う部分ではブラックボックスになって見えないようにする、とかかな。
↓のコードは、ボタン処理とかダウングレード処理を除いて1/3ぐらいを切り取ったもの
この辺だけで頭が悪いコードだってことと、改善案が適用できそうなことがわかると思う。
void UpGradeStatus(string status) { switch (status) { case"MaxStock": UpGradeMaxStock(); break; case "ShootSpan": UpGradeShootSpan(); break; case "LockOnSpan": UpGradeLockOnSpan(); break; case "LockOnRadius": UpGradeLockOnRadius(); break; case "ForceMagnitude": UpGradeForceMagnitude(); break; case "AttackPoint": UpGradeAttackPoint(); break; case "SpawnSpan": UpGradeSpawnSpan(); break; case "LaserDamage": UpGradeLaserDamage(); break; case "SupportCharacter": UpGradeSupportCharacter(); break; case "LaserCount": UpGradeLaserCount(); break; default: //Debug.LogWarning(status + " is invalid status in FactoryButtonController.cs UpGradeStatus()"); break; } gameObject.GetComponent<FactoryUIController>().UpdateUI(); //ボタンの状態を設定 UpdateButtonStatus(); UpdateLimitStatus(); } bool PaymentUpGrade(string status) { if(this.factorySceneController.Money >= UpGradeCosts[status]) { this.factorySceneController.Money -= UpGradeCosts[status]; return true; } else { return false; } } //ボタンがアクティブかどうかをアップデートする void UpdateButtonStatus() { foreach(KeyValuePair<string,double> cost in this.UpGradeCosts){ if(this.factorySceneController.Money < cost.Value) { this.buttonDictionary[cost.Key].GetComponent<Button>().interactable = false; } else { this.buttonDictionary[cost.Key].GetComponent<Button>().interactable = true; } this.buttonDictionary[cost.Key].transform.Find("Text (TMP)").gameObject.GetComponent<TextMeshProUGUI>().text = $"${cost.Value,-4}"; } } //ステータス上限を適用 void UpdateLimitStatus() { //ArrayList[string,float] = {UpperLimit/LowerLimit, value}; foreach (KeyValuePair<string,ArrayList> limit in this.maxStatus) { //Debug.Log((string)limit.Value[0]); //Debug.Log((float)limit.Value[1]); Debug.Log(GetStatus(limit.Key)); switch ((string)limit.Value[0]) { case "UpperLimit": if((float)limit.Value[1] <= GetStatus(limit.Key)) { this.buttonDictionary[limit.Key].GetComponent<Button>().interactable = false; } break; case "LowerLimit": if ((float)limit.Value[1] >= GetStatus(limit.Key)) { this.buttonDictionary[limit.Key].GetComponent<Button>().interactable = false; } break; default: Debug.LogWarning(limit.Value[0] + " is invalid status in FactoryButtonController.cs UpdateLimitStatus"); break; } } } void UpGradeMaxStock() { this.factorySceneController.MaxStock++; } void UpGradeShootSpan() { this.factorySceneController.ShootSpan *= 0.95f; } void UpGradeLockOnSpan() { this.factorySceneController.LockOnSpan *= 0.95f; } void UpGradeLockOnRadius() { this.factorySceneController.LockOnRadius += 10f; } void UpGradeForceMagnitude() { this.factorySceneController.ForceMagnitude *= 1.1f; } void UpGradeAttackPoint() { this.factorySceneController.AttackPoint += 5f; } void UpGradeSpawnSpan() { this.factorySceneController.SpawnSpan *= 0.95f; } void UpGradeLaserDamage() { this.factorySceneController.LaserDamage += 0.5f; } void UpGradeSupportCharacter() { this.factorySceneController.SupportCharacter++; } void UpGradeLaserCount() { this.factorySceneController.LaserCount++; }
7日目
ここでは要素の追加とunityroomで公開してくれてるランキングDB連携を実装したはず。そんでアップロード
支援ヘリ
なんてことはなく、プレイヤーに適用させていたマウスカーソルの中心からの移動量で向きを変えたりだとか、ミサイル発射位置を設定しておいてそこから発射するだとかのスクリプトを適用しただけ。
そして↓のスクリプトをくっつけておいて、ステータスのアップグレード状況に応じて見えたり見えなかったりするようにした。
void Start() { GameObject[] supports = GameObject.FindGameObjectsWithTag("PlayerSupport"); for(var i = supports.Length; i > GetComponent<BattleSceneController>().SupportCharacter; i--) { supports[i - 1].SetActive(false); } }
レーザー発射(フルバースト)
最初は時間もないしレーザーだけにしようかと思っていたんだけど、ミサイル発射ロジック流用すれば簡単にフルバーストできそうだったので追加した。
なのでクラス名はLaserShooterのまま
やっていることとしては、
⓪レーザー用エフェクトを作成
(大きな弾を連続して発射するようにして、それぞれに衝突時の判定をさせている。)
①レーザー発射ロジック記述(タメ&ミサイルのターゲティングを全方向ランダムに→レーザーインスタンスを既定の位置に生成&ミサイル発射)
②別クラスで、レーザーのチャージと発射可能判定をするクラスを用意
③②に応じてボタンを有効化/無効化する
という感じ。長いし中の処理解説するの面倒なので割愛。
しようかと思ったけど、せっかくなのでフルバーストロジック部分だけ記述しておく。
public class LaserShooter : MonoBehaviour { ChargeLaser chargeLaser; [SerializeField] List<GameObject> laserParticle; [SerializeField] List<GameObject> laserPosition; GameObject laser; //フルバースト用ミサイル [SerializeField] GameObject missilePrefab; [SerializeField] List<GameObject> missilePositionList; List<GameObject> targetList; int missileStock; bool canShootMissile = false; ShootMissile shootMissile; [SerializeField] float maxMissileShootRotationY = 60f; [SerializeField] GameObject lockOnAudio; List<GameObject> targetIconList; //ゲームマネージャ取得 GameObject gameManager; //SE再生 AudioSource audioSource; [SerializeField] AudioClip laserChargeSound; [SerializeField] AudioClip laserShootSound; // Start is called before the first frame update void Start() { chargeLaser = GetComponent<ChargeLaser>(); //SE準備 audioSource = GetComponent<AudioSource>(); //フルバースト用ターゲット targetList = new List<GameObject>(); targetIconList = new List<GameObject>(); //ミサイル発射スクリプト取得 shootMissile = GetComponent<ShootMissile>(); //ゲームマネージャ取得 gameManager = GameObject.FindWithTag("GameManager"); missileStock = gameManager.GetComponent<BattleSceneController>().MaxStock; } public void ShootLaser() { canShootMissile = false; //laser.SetActive(true); for (var i = 0; i < gameManager.GetComponent<BattleSceneController>().LaserCount; i++) { laser = Instantiate(laserParticle[i], laserPosition[i].transform.position, laserPosition[i].transform.rotation); laser.transform.SetParent(gameObject.transform); } StartCoroutine(LockOnTarget()); chargeLaser.SetInitCharge(); //チャージSE再生 audioSource.PlayOneShot(laserChargeSound); } IEnumerator LockOnTarget() { StartCoroutine(Delay()); int count=0; while(count<missileStock) { GameObject[] targets = GameObject.FindGameObjectsWithTag("Enemy"); //targetList.Add(targets[count]); AddSearchTargetToTargetList(targets, targetList); this.lockOnAudio.GetComponent<AudioSource>().Play(); count++; //UIに残弾を反映 gameManager.GetComponent<BattleUIController>().SetBulletStock(count, missileStock); if (canShootMissile) { break; } yield return new WaitForSeconds(0.05f); } //UIに残弾を反映 gameManager.GetComponent<BattleUIController>().SetBulletStock(missileStock, missileStock); StartCoroutine(ShootTarget()); } IEnumerator ShootTarget() { int stockCount=0; while(canShootMissile == false) { //this.lockOnAudio.GetComponent<AudioSource>().Play(); yield return new WaitForSeconds(0.05f); } //シュートSE再生 audioSource.PlayOneShot(laserShootSound); //ターゲットアイコンを破棄 foreach(GameObject icon in targetIconList) { Destroy(icon); } while (stockCount < missileStock) { foreach(GameObject position in missilePositionList) { Instantiate(missilePrefab, position.transform.position, CalcShootMissileRotation(position.name,stockCount)); stockCount++; //UIにストックを反映 gameManager.GetComponent<BattleUIController>().SetBulletStock(missileStock - stockCount, missileStock); if ( (stockCount < missileStock) == false ) { break; } } yield return new WaitForSeconds(0.1f); } canShootMissile = false; targetList = new List<GameObject>(); targetIconList = new List<GameObject>(); } IEnumerator Delay() { yield return new WaitForSeconds(2.0f); canShootMissile = true; } Quaternion CalcShootMissileRotation(string missilePositionName, int missileCount) { int count = missileCount / 2; int stock = missileStock / 2; float rate = (float)count / (float)stock; float rotationY = rate * maxMissileShootRotationY; Vector3 rotationEuler = new Vector3(-20f, rotationY, 0); if (missilePositionName.Contains("Left")) { rotationEuler = new Vector3(rotationEuler.x, -rotationEuler.y, rotationEuler.z); } return Quaternion.Euler(rotationEuler); } void AddSearchTargetToTargetList(GameObject[] enemies, List<GameObject> targets) { int index = 0; int prevCount = targets.Count; while (index < enemies.Length && targets.Count == prevCount) { if (targets.Contains(enemies[index]) == false) { targets.Add(enemies[index]); //UIにターゲットアイコンを生成 targetIconList.Add(gameManager.GetComponent<BattleUIController>().CreateTargetEnemyIcon(enemies[index])); } index++; } } }
多分残弾UIとかミサイル発車とか、もっと役割を分けておけばコードが重複することもなかったかなと。
まぁでも想定外の動きとかは無いのである程度うまく記述はできてるんじゃないかな?
というのも、大抵設計が悪いと動作の調子も悪いのでこれはほとんど希望的観測
ランキングDB連携
これはunityroomにやり方が載ってる。
解説なし!w
感想
チカレタ...(;´Д`)
でもはじめてにしては結構頑張ったと思う。
でも頑張っただけだから次は「ゲーム」を作りたいよね。これゲーム性ほとんどないから!
だからもっと最初の時点で時間をとるべきだと思った。ゲーム性を練るというか、時間がいくらでもあるんだったら作りながら考えていけばいいし、むしろそっちのほうが良いと思うけど、ミニゲームとかはゲーム性が命だから!その他挙動のこだわりはあるに越したことはないけど、ゲーム性に沿ったこだわりにしたほうが完成度が高まると思う。
んで次とか言ってるけど多分次の1週間ゲームジャムは、よっぽどモチベーション高くない限りは参加しないと思う。
今はキャラデザとかドット絵アニメーションとか作ってみながら、デビルメイクライ的なゲーム性で2D横スクロールなゲームを作ろうと思ってる。
シナリオは、ヒロインの親が研究所所長な感じで、だいぶ前に事故死判定なヒロインを生き返らせるためにマッドな研究にはまって、やばい寄生生物が逃げ出してそのうちそんなに力強くなさそうなやつが偶々ヒロイン見つけて、ヒロインの未完成な体を補いながらいい感じにwin-winするみたいなイメージ。
承転結全然考えてないけど...まぁ何とかなるやろ!
今週もお疲れ様でした...まだ月曜日だった。しんど(´Д`)ハァ…