概要・タイトル画面【(悪人)もぐらたたき】爆発を呼ぶ男【刑事・西門郷介】の作り方(1)【Unity】
1.コンセプト
(1)ゲームコンテストへの参加
どうもしょう@ゆっくり学ぶチャンネルです。
7月の1カ月でゲームを1本作るコンテストに参加することになりました。
で、1カ月で作れる規模のゲームなので、そんなに大規模なゲームや時間がかかりそうなゲームは作れません。
まずゲームジャンルはもぐらたたきゲームにしました。システムが単純で老若男女問わずとっつきやすいからです。
もぐらたたきで1番ヒットしていると思われるアプリは「ふつうのもぐらたたき」です。
モグラたたきの講座もいくつかヒットしました(今回はほとんど参考にできませんでしたが…)。
https://odevnote.net/article/simple_mole_assets
(2)西部警察とモグラたたきの融合
ただ、普通にもぐらたたきにしてもありきたりだし、先述の「ふつうのもぐらたたき」に勝てません。
なので、中高年の男性に刺さるもぐらたたきを目指しました。
中高年の男性って西部警察好きですよね(偏見)。
自分は世代ではないのですが、結構好きです。
「こんなの毎週やってたの!?」って感じです。
もう無茶苦茶だけど、格好いい。派手。特に寺尾聡さん。
コンプラだのポリコレだの無縁なあの感じを、ほんの少しだとしてもゲームで再現できないかと思いました。
「悪人がモグラみたいに地面から出てくる」
「タップすると悪人が爆発して〇ぬ」
「悪人は早くタップしないと撃ってきて、プレイヤーが〇ぬ」
こんなゲームならば、モグラたたきの要領で作れるし、他のゲームとの差別化も図れる、何よりバカバカしくて面白そう!こんな感じでゲーム作りが始まりました。
(3)西門郷介の誕生
ゲームの題名は「【(悪人)もぐらたたき】爆発を呼ぶ男 刑事・西門郷介」としました。
西門はサイモンです。
もちろん西部警察で渡哲也さんが演じた大門圭介が元ネタです。響きはそのままに頭文字を西部警察の西に変えました。我ながら気に入っています。
フリーフォント「チェックポイント★リベンジ」を赤にして、入力した言葉の中央が高くなるようにすると、西部警察のロゴっぽくなります。背景も適度に黒く塗ってください。
2.オープニング
(1)パトカーと車が走ってくる演出
オープニングはこんな感じです。
これには元ネタがあり、転生前の西部警察「大都会PARTⅢ」のオープニングです。
どうやってオープニングを作ったかですが、単純にスタートと同時に車を走らせただけです。オブジェクトの配置は以下の画像の通りです。
背景の街の部分は「ぱくたそ」のフリー画像で看板等の権利関係が発生しそうなものが写っていないモノをチョイスし、空の部分をCLIP STUDIO PAINTで消しました。
Canvasのレンダーモードをワールド空間にすると、画像やテキストをワールド空間に置くことができます。
パトカーと逃走車の共通コードは以下の通りです。
using UnityEngine;
public class PatrolCarController : MonoBehaviour
{
public GameObject sirenObject; // サイレン用のオブジェクト
public float speed = 5f; // 前進する速度
public float patrolInterval = 2f; // サイレンの間隔
public float groundCheckDistance = 0.2f; // 接地判定の距離
public float downForce = 10f; // 下方向の力の大きさ
private bool isMovingForward = true; // 前進中かどうかのフラグ
private float timer; // タイマー
private Rigidbody rb; // Rigidbodyコンポーネント
[SerializeField] private Vector3 localGravity;
private bool _b;
private void Start()
{
_b = IsGrounded();
// Rigidbodyコンポーネントを取得
rb = GetComponent<Rigidbody>();
rb.useGravity = false; //最初にrigidBodyの重力を使わなくする
}
private void Update()
{
// パトロールカーを前進させる
if (isMovingForward)
{
Vector3 movement = transform.forward * (speed * Time.deltaTime);
rb.MovePosition(rb.position + movement);
// 地面に接地していない場合、下方向に力を加える
if (!_b)
{
Vector3 downForceVector = Vector3.down * downForce;
rb.AddForce(downForceVector, ForceMode.Force);
}
}
if (this.transform.position.y <= -300){
Destroy(this);
}
//サイレンオブジェクトがない(つまり逃走車)ならリターン
if(!sirenObject)return;
// タイマーを更新
timer += Time.deltaTime;
if (timer >= patrolInterval)
{
// タイマーが指定の間隔を超えたら切り替える
ToggleSiren();
// タイマーをリセット
timer = 0f;
}
}
private void ToggleSiren()
{
// サイレン用のオブジェクトのSetActiveを切り替える
sirenObject.SetActive(!sirenObject.activeSelf);
}
private bool IsGrounded()
{
// 下方向にレイキャストを発射して接地判定を行う
float raycastDistance = GetComponent<Collider>().bounds.extents.y + groundCheckDistance;
if (Physics.Raycast(transform.position, Vector3.down, out _, raycastDistance))
{
return true;
}
return false;
}
private void FixedUpdate ()
{
SetLocalGravity (); //重力をAddForceでかけるメソッドを呼ぶ。FixedUpdateが好ましい。
}
private void SetLocalGravity()
{
rb.AddForce (localGravity, ForceMode.Acceleration);
}
}
ゲーム開始と同時に車が
Vector3 movement = transform.forward * (speed * Time.deltaTime); rb.MovePosition(rb.position + movement);
により前進します。
パトカーのサイレンの前にはスポットライトをつけた子オブジェクトがあり、これのSetActiveを切り替えることで、サイレンの点滅を表現しています。
逃走車はサイレンがないので、
//サイレンオブジェクトがない(つまり逃走車)ならリターン if(!sirenObject)return;
の部分でUpdateが終わり、エラーが出ないようにします。
SetLocalGravity()や
Vector3 downForceVector = Vector3.down * downForce; rb.AddForce(downForceVector, ForceMode.Force);
については、下り坂で車が吹っ飛ぶのを試行錯誤した結果こうなりました。
だがその他一切の事は分かりません!
車が前進する先にはDestroyAreaがあり、西部警察よろしく車は破壊されることになりますが、それが「はじまりの合図」です(事項に続く)。
(2)スキップボタンの実装
以下のStartManagerをTitleシーンの空オブジェクトにアタッチします。
パトカーのうち一台をGame Object carとして登録、carをUpdate関数で監視し、carが先述のDestroyAreaで爆散し、car==nullになったら、TitleStartのコルーチンがスタートし、タイトルのUI類が表示されます。
using UnityEngine;
using System.Collections;
using DG.Tweening;
public class StartManager : MonoBehaviour{
public GameObject car;//パトカーのうち一台を監視
private bool opningStart;//オープニングが始まったか
public AudioClip bgm;
public AudioClip titleSE;
public RectTransform bakuhatsuText;
public RectTransform keijiText;
public RectTransform saimonText;
public RectTransform sakushaText;
public CanvasGroup buttons;//ボタングループ
public bool skip;//スキップボタンが押されたかどうか
public AudioSource carSound;//逃走車のオーディオソース
public AudioSource patrolCarSound;//パトカーのオーディオソース
public GameObject skipButton;//スキップボタン
public void Start(){
//ボタンを非活性にする。ボタンが移動してくる前に押せてしまうため。
buttons.interactable = false;
}
private void Update(){
//オープニングがスタートしていたらリターン
if (opningStart) return;
//スキップボタンが押されたらスキップ用コルーチン実行
if (skip){
StartCoroutine(nameof(SkipStart));
}
//監視している車がなくなった(Destroyされた)らタイトル用コルーチン実行
if (!car){
StartCoroutine(nameof(TitleStart));
}
}
//スキップボタンが押されたら実行する。スキップボタンに登録。
public void OnClickSkipButton(){
skip = true;
}
IEnumerator TitleStart() {
//オープニングスタート
opningStart = true;
//スキップボタンを無効にする
skipButton.SetActive(false);
//西門郷介の文字(画像)が起き上がる
saimonText.DORotate(new Vector3(0, 0, 0), 1.5f); // 起き上がる
yield return new WaitForSeconds(0.5f);//0.5秒待つ
SoundManager.instance.PlaySe(titleSE);//爆発SE
yield return new WaitForSeconds(1.0f);//1秒待つ
//タイトルの文字や画像が横から移動してくる。
bakuhatsuText.DOLocalMoveX(-35, 0.3f).SetEase(Ease.Linear); // スライドする
keijiText.DOLocalMoveY(55f, 0.3f).SetEase(Ease.Linear); // スライドする
sakushaText.DOLocalMoveX(400f, 0.5f).SetEase(Ease.InOutBounce); // スライドする
yield return new WaitForSeconds(0.5f);//0.5秒待つ
//BGMスタート
SoundManager.instance.PlayBgm(bgm);
//ボタンがフェードして現れる。
buttons.DOFade(endValue: 1f, duration: 1.2f);
//ボタンを活性化し、押せるようにする。
buttons.interactable = true;
yield return null;
}
IEnumerator SkipStart() {
//オープニングスタート
opningStart = true;
//スキップボタンを無効にする
skipButton.SetActive(false);
//パトカーと逃走車の音を無効にする
carSound.enabled = false;
patrolCarSound.enabled = false;
//タイトルの画像、文字、ボタンを0fで表示する。
saimonText.DORotate(new Vector3(0, 0, 0), 0f); // スライドする
SoundManager.instance.PlaySe(titleSE);
bakuhatsuText.DOLocalMoveX(-35, 0.0f).SetEase(Ease.Linear); // スライドする
keijiText.DOLocalMoveY(55f, 0.0f).SetEase(Ease.Linear); // スライドする
sakushaText.DOLocalMoveX(400f, 0.0f).SetEase(Ease.InOutBounce); // スライドする
buttons.DOFade(endValue: 1f, duration: 0f);
buttons.interactable = true;//ボタン活性化
yield return new WaitForSeconds(1.5f);
//BGMスタート
SoundManager.instance.PlayBgm(bgm);
yield return null;
}
}
オープニング演出が作成できたのですが、流石に10秒近くある、これを何度も見るのはプレイするのもデバッグするのもしんどいので、スキップボタンを実装しました。
スキップボタンにOnClickSkipButton()を登録します。
OnClickSkipButtonはSkipをtrueにし、SkipStartのコルーチンが開始します。SkipStartはTitleStartのコルーチンとほぼ同じですが、パトカーと逃走車のオーディオソースを無効にしています(事項に続く)。
(3)遠くから近づいてくる音を再現する
自分は基本的にSoundManagerにあらかじめ登録してあるAudioSouce(BGM用、SE用、Voice用)を使用しているため、オブジェクトにAudioSouceはつけませんが、SoundManagerに登録しているAudioSouceはAudioListener(基本的にはMainCameraにアタッチされている)との距離が一定なため、パトカーと車の音が遠くから近づいてくる演出ができません。
なのでオープニングのパトカーと逃走車にはAudioSouceを独自に持たせています。
出力をSEにすることで、効果音のボリューム調節の影響を受けさせることができます。
なお、パトカーはAudioSouceを持たせているのは1台で、サイレンの効果音をAudacityで重ねたmp3データを流しています。理由は1台のAudioSouceを取得すれば済むからです。
<参考>自分が使用しているSoundManager
(4)完成間近で床をすり抜けて落下する車たち――Layer Collision Matrixを安易にいじるな
しばらくメインのシーンを作って、タイトルシーンに戻ったら車が地面を突き抜けて、全て奈落の底に落ちるトラブルが発生しました。
タイトルシーンはいじっていないのに何故?
Layer Collision MatrixでDefaultLayer同士のCollisionのチェックを外してしまったことが原因でした…。
Layer Collision Matrixは便利ですが、取り扱いは慎重に。
3.メニューの演出
(1)ボタンがスライドしてくる演出と早押し防止
タイトル画面で、ボタン(CanvasGroup)がDotweenでフェードしてくる前に押せてしまいました。
フェード前も色がないだけで、ボタンは押せる状態だからでした。
StartManagerのStartでCanvasGroupのInteractableをfalseにし、ボタンを押せるタイミングになったらtrueにすることで問題は解決しました。
(2)ボタンを押すと弾痕が入る演出
ボタンをクリックしたら弾痕が入る演出については子オブジェクトのActiveSelfをTrueにしているだけです。
そして、Invoke関数を使用し、親オブジェクトであるボタンと子オブジェクトである弾痕のActiveSelfを同時にfalseにすれば実現できます。
private void EffectGameObject(){
//エフェクト用のGameObjectがなければリターン
if (!effectObject)return;
effectObject.SetActive(true);//エフェクト用オブジェクト表示
_button.interactable = false;//ボタンを非活性化
}
private void EffectGameObjectHide(){
if (!effectObject)return;
effectObject.SetActive(false);//エフェクト用オブジェクト非表示
_button.interactable = true;//ボタンを活性化
}
(3)黒くなるText(フォント)の対応方法
Textはエディター上でなぜか真っ黒になってしまうことがありますが、「FontMaterial」をどれでもいいのでアタッチすると、直ります。
(4)インタースティシャル広告後に停止する不具合の解決方法
インタースティシャル広告が入った後のシーンでタイムスケールがなぜか0になってしまいましたが、インタースティシャルが呼び出せたタイミングでタイムスケールを1にすると解決しました。
private void AdInterstitial(){
if (interstitial){
AdMobInterstitial.instance.ShowAdMobInterstitial();
Time.timeScale = 1;//戻らない時があったため入れる。
}
}
次回はステージ作成とVroidについてです。