【Unity】シューティングを作った~振り返り~
ご!ピクジンです。
前々からコードが破れていて買い換えなきゃな〜と思って頼んだMacBookの充電ケーブルが届きました。
変なの掴まされたくなかったので純正品にしましたが、税含めて8000円超えと結構高い。痛い出費です。
昨日の予告通り、今回は2週間ちょいで作った節分シューティングの振り返りをしていこうと思います。
ゲーム概要
改めてゲームの紹介をしていきましょう。
タイトル:オニハソト
ジャンル:シューティング
迫りくる鬼っぽい奴らを撃ちまくれ!
マウスのみで操作できるシンプルシューティングゲームです。
一応スマホからでも遊べます。
ただし右クリックが困難なのでおすすめはしません。
■コンセプト:
・オーソドックスなシューティングゲーム
・節分の要素を盛り込む
■システム:
・左クリックでショット(押しっぱなしで連射)
・敵を倒すとエネルギーを落とす
・エネルギーを回収すると左上の必殺技ゲージが溜まる
・ゲージが溜まったら右クリックでビームを発射
・ビームの向きは左上にある方位磁針の赤い方と同じ向きに発射される
・方位磁針の向きは一定時間ごとにランダムに決定される
・ビームを撃つ度、次のビーム発射に必要なエネルギー量が1増える
豆撒き→シューティング
年の数だけ豆を食べる→エネルギー
恵方→ビームの向き
をイメージしています。
今回の目的
1.節分をテーマにしたゲームを作る
Unity1Weekが終わって随分経ち、そろそろ新しいゲームを作らなければいけないけどネタがない......と悩んでいた時にこちらの動画を観ました。
季節ネタでやってみるのはいいかもなぁ〜っと、呑気に思いながら直近の季節イベントである節分をテーマにして考えてみました。
2.シューティングゲームを作る
テーマを節分に決めた時、真っ先に思いついたのが恵方巻でした。
毎年違う向き(これを恵方*と言う)を向いて食べるアレですね。
*恵方とは、歳徳神という神様のいる方角です。
今年は南南東でした。な、なんと(激寒駄洒落)
もしも、毎年変わる恵方のように、撃つ方向がランダムに変わるビームがあったらどうだろうか?
こんな思いつきで節分のシューティングゲームに決定しました。
そもそも、テーマを決める以前からシューティングゲームを作ってみたいと考えていました。
理由は邪ですが、「シューティングはつまらなくなりようがない」というツイートを目にしたからです。
あと王道ジャンルですから経験しておきたかったのです。
3.UniTaskを使ってみる
Unity1Weekの参加者が結構使ってるっぽいUniRxやらUniTaskやらには前々から興味があったので、隙があれば使ってやろうなどと考えていました。
UniRxはいまいちピンと来なかったので、強いコルーチンと呼ばれてるUniTaskだけ使いました。
Task自体の理解がなかったこともあり、最初は上手く扱えなかったので一旦使うのやめようと思っていましたがなんとかなるものです。
以下が使用例です。
//敵生成クラス
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using Cysharp.Threading.Tasks;
public class proEnemySpawn : MonoBehaviour
{
[SerializeField]
private List<string> fileName = new List<string>();
TextAsset csvFile;
List<string[]> waveDate = new List<string[]>();
[SerializeField]
private int maxColumn = 11;
[SerializeField]
private float horizontal_interval = 0.5f;
[SerializeField]
private int phase_num = 0;
int wave_num = 0;
[SerializeField]
private List<Phase> phases = new List<Phase>();
[System.Serializable]
class Phase
{
public List<int> pattern = new List<int>();
}
[SerializeField]
private float spawn_interval = 0.5f;
[SerializeField]
private proEnemy enemyPrefab = null;
[SerializeField]
private proPlayer player = null;
bool boss_battle = false;
public bool Boss_Battle
{
set { boss_battle = value; }
}
[SerializeField]
private List<proBoss> boss = new List<proBoss>();
[SerializeField]
private scoreText score_text = null;
[SerializeField]
private GameController gameController = null;
int player_miss_count = 0;
[SerializeField]
private AudioSource SE = null;
public void SpawnSetting()
{
if(phase_num >= phases.Count)
{
gameController.GameEnd();
return;
}
if (phase_num % 2 == 0)
{
_ = SpawTask();
}
else
{
if (boss[(phase_num + 1) / 2 - 1] == null)
{
phase_num++;
SpawnSetting();
return;
}
boss_battle = true;
if(boss[(phase_num + 1) / 2 - 1].gameObject.activeSelf)
{
BossBattle();
}
else
{
boss[(phase_num + 1) / 2 - 1].gameObject.SetActive(true);
}
}
}
private async UniTask SpawTask()
{
int num = wave_num;
for (int i = num; i < phases[phase_num].pattern.Count; i++)
{
print(phases[phase_num].pattern[i]);
if (!player.gameObject.activeSelf)
{
wave_num = i;
return;
}
await CSVload(phases[phase_num].pattern[i]);
}
phase_num++;
wave_num = 0;
SpawnSetting();
}
private async UniTask SpawRoop()
{
while (player.gameObject.activeSelf)
{
//ここらへんにボス倒した時のあれこれを仕込む
if (!boss_battle)
{
score_text.ScoreUpdate(50000 * phase_num);
playerInvisible(5.0f);
phase_num++;
return;
}
int rum = Random.Range(0, phases[phase_num].pattern.Count);
await CSVload(phases[phase_num].pattern[rum]);
}
}
private async UniTask CSVload(int num)
{
csvFile = Resources.Load("Wave/" + fileName[num]) as TextAsset;
StringReader reader = new StringReader(csvFile.text);
while (reader.Peek() != -1)
{
string line = reader.ReadLine();
waveDate.Add(line.Split(','));
}
await spawn();
}
IEnumerator spawn()
{
print("spawnコルーチン:" + waveDate.Count);
for (int line = waveDate.Count - 1; line >= 0; line--)
{
//print("ライン:" + line);
for (int column = 0; column < maxColumn; column++)
{
//print("列:" + column);
int num = int.Parse(waveDate[line][column]);
if (num >= 0)
{
float f = column - (maxColumn - 1) / 2;
Vector2 spanPos = new Vector2(f * horizontal_interval, transform.position.y);
proEnemy enemy = Instantiate(enemyPrefab, spanPos, Quaternion.identity);
enemy.ParameterSet(num, score_text, SE);
}
}
yield return new WaitForSeconds(spawn_interval);
}
waveDate.Clear();
}
public void BossBattle()
{
_ = SpawRoop();
}
}
SpawnTask()は決められた順番に敵を生成する処理です。
決められた生成を全て完了したら次のフェーズに移行します。
プレイヤーがミスしてたら生成を中止します。
private async UniTask SpawTask()
{
int num = wave_num;
for (int i = num; i < phases[phase_num].pattern.Count; i++)
{
print(phases[phase_num].pattern[i]);
if (!player.gameObject.activeSelf)
{
wave_num = i;
return;
}
await CSVload(phases[phase_num].pattern[i]);
}
phase_num++;
wave_num = 0;
SpawnSetting();
}
SpawnRoop()(スペルミスってるわ恥ずかし...)はボス敵が倒されるまでランダムに敵を生成し続ける処理です。
プレイヤーが生きてる間続き、ボスが倒されたら次のフェーズに移行します。
private async UniTask SpawRoop()
{
while (player.gameObject.activeSelf)
{
//ここらへんにボス倒した時のあれこれを仕込む
if (!boss_battle)
{
score_text.ScoreUpdate(50000 * phase_num);
playerInvisible(5.0f);
phase_num++;
return;
}
int rum = Random.Range(0, phases[phase_num].pattern.Count);
await CSVload(phases[phase_num].pattern[rum]);
}
}
上記の2つともCSVload(int num)の処理が終わるまで待機し、CSVload自身はspawn()コルーチンが終わるまで待機します。
private async UniTask CSVload(int num)
{
csvFile = Resources.Load("Wave/" + fileName[num]) as TextAsset;
StringReader reader = new StringReader(csvFile.text);
while (reader.Peek() != -1)
{
string line = reader.ReadLine();
waveDate.Add(line.Split(','));
}
await spawn();
}
IEnumerator spawn()
{
print("spawnコルーチン:" + waveDate.Count);
for (int line = waveDate.Count - 1; line >= 0; line--)
{
//print("ライン:" + line);
for (int column = 0; column < maxColumn; column++)
{
//print("列:" + column);
int num = int.Parse(waveDate[line][column]);
if (num >= 0)
{
float f = column - (maxColumn - 1) / 2;
Vector2 spanPos = new Vector2(f * horizontal_interval, transform.position.y);
proEnemy enemy = Instantiate(enemyPrefab, spanPos, Quaternion.identity);
enemy.ParameterSet(num, score_text, SE);
}
}
yield return new WaitForSeconds(spawn_interval);
}
waveDate.Clear();
}
コルーチンが終わるのを待てるのでなかなかに便利でした。
何やらDOTweenもawaitできるらしいのですが、やりかたがよくわからなかったので今回はパスしました。
リベンジしたいところ。
反省点
反省点はいつもの通り、「無計画で見切り発車な行動を慎め」ってことです。
1.見た目にこだわり過ぎ
ゲームを面白そうと思わせるものは見た目なんですが、ゲームの主要部分が出来てもいないうちから大して描けもしない絵を描いたりするもんじゃないです。
自分自身見た目に対するこだわりが強い(実力は伴わない)とわかってるので、優先順位の確認を怠らないようにしていきたいです。
2.設計が雑
クラス設計まできっかりしようとは思いませんが、どんなコードにするかを書きながら決めてばかりなのはいかがなものかと思います。
なんとなーくふんわりしか決めずに突っ走る企画力のなさに起因しているのかもしれません。だからこそ動けるのかもしれませんが......
3.とにかく時間の使い方が下手
上記のことに加え、締め切りから逆算して作業時間を決めたりできないのがよくありません。
特に作業し始めるのが遅いことと、いざ始めたらなかなか終わりにしないことは直していきたいですね。
メリハリのある行動ってやつです。
終わり
1番改善しなければいけないのは、早い段階からゲームの見た目をこだわりすぎないようにすることですね。
ゲームと呼べる最低限度のものを素早く作ることを最優先にしなければいけないと思います。
それが出来てからゆっくりと見た目をこだわっていけばいいのです。
優先順位大事。
ともかくいい経験にはなりました。
あと適当に作ってもシューティングはそれなりに楽しいっていうのがよくわかりました。
またこんど。