ぐーるらいふ

しがないリーマンの記録。遊びのunityのメモ帳にしたい。

【Unity】第10回unity1week「10」ベルトスクロールアクションシステムについて

お疲れ様です。ぐーるです。
今回はunity1weekで提出した
「School Bag Fight」
f:id:ghoul_life:20181127003802p:plain
School Bag Fight | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
についての記事にしようと思います。

お題について

また困るやつ…w
勝手に「クリスマス」とか「お正月」
とか思ってたんですが、そんなものではなかった…w
まぁ、とりあえず作りたいものを作って
後から帳尻を合わせようかな、何か作りたいものはあるかな?
と言う所から考えることにしました。

作りたいもの?

挑戦したかったのは自分で描くスプライトアニメーションでした。
でとりあえず描き始めたのが徒歩モーション。

f:id:ghoul_life:20181127211549p:plain

これが完成した辺りで、そうだファイナルファイトみたいな
ベルトスクロールアクションを作ろうかなと思いました。

コアシステムについて

日本ではベルトスクロールと言いますが、
海外ではBeat em upとか言うのかな。

一見複雑に見えます(?)が、実はソースコードはそこまで長くはならなかったです。

四つのシステム

本当のコアの部分は以下の四つのクラスで構成されています。
一部抜粋で冗長になりそうなnullチェックとかrangeチェックとか消してます。

AreaManager
EnemyFactory
Player
Enemy

AreaManager

いきなりですけどコードを。

    public Camera _mainCamera;
    private int _index;
    private bool _isNext;
    private static Area[] _areas = new Area[]
    {
        new Area(-20.0f, -0.5f, -4.0f),
        new Area(-17.0f, -0.5f, -4.0f),
        new Area(-8.0f, -0.5f, -4.0f),
        new Area(-3.0f, -0.5f, -4.0f),
        new Area(2.0f, -0.5f, -4.0f),
        new Area(7.0f, -0.5f, -4.0f),
        new Area(12.0f, -0.5f, -4.0f),
        new Area(18.0f, -0.5f, -4.0f),
        new Area(23.0f, -0.5f, -4.0f),
        new Area(28.0f, -0.5f, -4.0f),
    };
    private Area _area;

    // 次のエリアへ移動可能
    public void Next()
    {
        _index++;
        _isNext = true;
        _area = _areas[_index];
    }

    void Update()
    {
        if (_isNext)
        {
            var p = _mainCamera.transform.position;
            if (p.x >= _area._targetX)
            {
                // 次のエリアへ到達。敵出現開始
                _isNext = false;
                EnemyFactory.Instance.NextStageStart();
            }
        }
    }

まずはAreaという概念から説明します。
ベルトスクロールアクションはよく

進む->止まって敵がワラワラと出てくる->全部倒したら->また次に進めるようになる

というイベントがあります。
これを1Areaとして管理することにしました。

Areaは

・目標X位置
・右端移動可能位置(算出)
・左端移動可能位置(算出)
・上に移動可能な位置
・下に移動可能な位置

とrectと似たようなデータを持つクラスです。
AreaManagerはこれをArea数分持っています。
そしてAreaの切り替わりはCameraの位置によって決めています。

1. 敵を全部倒した
2. AreaManagerの次のエリアデータをセットして、次に進んでいいよフラグを立てる
3. フラグが立っている時は目標X位置にカメラが来るまで待機
4. 目標X位置までカメラが到達したら進んでいいよフラグを降ろし敵を出現させる

という流れになります。

3.の状態の時、横着してまして、
左(Xマイナス方向)に進むことを出来なくしています。
CineMachineとか使ってたら範囲内ならプレイヤーだけ移動、範囲外に出たらカメラごと移動
という良いUXに出来たなーと後でちょっと思いました。今後の課題

EnemyFactory

読んで字のごとく敵を生成するクラスです。

    private List<EnemyScript> _enemyList;
    private List<Bullet> _bullets;
    private int _createCount = 0;
    private int _disposeCount = 0;
    private float _popTimer = 10;
    public bool _running = false;

    // int enemyCount, float moveSpd, float countDownBeforeSpd, float countDownAfterSpd, float popSpd 
    private StageData[] _stages = new StageData[]
    {
        new StageData(1, 100, 1, 10, 5),
        new StageData(3, 5, 2, 50, 3),
        new StageData(5, 5, 2, 50, 1),
        new StageData(1, 100, 1, 30, 3),
        new StageData(5, 10, 3, 30, 2),
        new StageData(10, 10, 2, 30, 1),
        new StageData(7, 3, 5, 50, 1),
        new StageData(7, 3, 4, 50, 0.5f),
        new StageData(10, 10, 7, 50, 2),
        new StageData(1, 100, 10, 100, 1),
    };

    private StageData _stage;

    // ステージを開始
    public void NextStageStart()
    {
        _running = true;
        _stage = _stages[AreaManager.Instance.Index];
    }

    public void CreateEnemy(StageData stage, Vector3 pos){ /* 敵を生成。リストに保存 */ }
    public void CreateBullet(bool fripX, Vector3 pos, Vector3 vector){ /* 弾を生成して発射。リストに保存 */ }

    public void DisposeEnemy(EnemyScript enemy)
    {
        if(enemy != null)
        {
            _disposeCount++;
            if (_enemyList != null)
            {
                _enemyList.Remove(enemy);
            }

            Destroy(enemy.gameObject);
            // 敵を倒したカウントを計測しておいて、出現数に到達したら次のエリアへ
            if(_disposeCount == stage._enemyCount)
            {
                _running = false;
                AreaManager.Instance.Next();
            }
        }
    }

    public void DisposeBullet(Bullet bullet){ /* リストから消してDestroy */}

    void Update () {
        if (_running)
        {
            if(_createCount < _stage._enemyCount)
            {
                _popTimer += Time.deltaTime;

                if (_popTimer > _stage._popSpd)
                {
                    _popTimer = 0;
                    var pos = _player.transform.position;
                    var rand = Random.Range(0, 100);
                    if (rand > 50)
                    {
                        pos.x += 10;
                    }
                    else
                    {
                        pos.x -= 10;
                    }
                    pos.x += Random.Range(-0.5f, 0.5f);
                    pos.y += Random.Range(-0.5f, 0.5f);

                    CreateEnemy(_stage, pos);
                    _createCount++;
                }
            }
        }
    }

EnemyFactoryはstageという情報を持っています。
Stageは

・敵の出現数
・敵の移動速度、攻撃速度
・敵のポップ速度

といった難易度パラメータを持っています。
この値とAreaが1:1で繋がっています。
(だったら一つのScriptableObjectにまとめとけ!って後で思いました)

この値に沿って敵を出現させるのですが、
まぁよくある手でListでEnemyもBulletも保持します。
これはOPに戻ったりした時にまとめてリセットをかけるために持ってます。
破棄する時に一緒にListからも外す必要が出てくるのでちょい面倒ではありますが、しょうがない。

Player

Playerは操作感というか
キーを離したらぴたっと止まるようにとか
攻撃速度を早くとかキックしているときだけ若干の硬直を入れてたりといった小細工を入れている程度なんですが、
一つだけ問題が出た所はContinueのフローでした。

f:id:ghoul_life:20181126235255p:plain
こちらPlayerのAnimatorになります。

敵にやられた時、ここのdown状態のStateで止まっていて
コンティニューを選んだ時にwaitに戻るのですが、最初はここをTriggerで管理してました。
一度目は良いのですが、二度目コンティニューをしようとすると即座にwaitに遷移してしまう、

というバグを生み出してしまいました。
なので、ここをfloatに変えて、値が一定以上ならwaitに戻り、戻ったら0で上書きしておく
と修正しました。

f:id:ghoul_life:20181127213507p:plain
死亡した時の画面。ゲーム全体を止めていて
発射モーション途中で止めたいと思ってそこは意識してます。

Enemy

State管理が重要です。

SEARCH
PATROL
ATTACK_BEFORE
SHOT
ATTACK_WAIT

の五つがあります。

        switch (_enemyState)
        {
            case EnemyState.SEARCH:
                _targetPosition = _player.transform.position;
                _targetPosition.x += Random.Range(-0.1f, 0.1f);
                _targetPosition.y += Random.Range(-0.1f, 0.1f);
                _enemyState = EnemyState.PATROL;
                break;
            case EnemyState.PATROL:
                _animator.SetFloat("walk", 1.0f);
                var move = _moveSpeed * Time.deltaTime;
                transform.position = Vector3.MoveTowards(transform.position, _targetPosition, move);
                if(Vector3.Distance(transform.position, _targetPosition) <= STOP_DISTANCE)
                {
                    _animator.SetFloat("walk", 0.0f);
                    _enemyState = EnemyState.ATTACK_BEFORE;
                    /* パラメータセット */
                }
                break;
            case EnemyState.ATTACK_BEFORE:
                if (_countdownTimer > 0)
                {
                    _countdownTimer -= _countdownBefore * Time.deltaTime;
                    // playerが離れすぎたら再度searchへ
                    if(Vector3.Distance(transform.position, _player.transform.position) > 8)
                    {
                        _enemyState = EnemyState.SEARCH;
                    }
                }
                else
                {
                    _enemyState = EnemyState.SHOT;
                    /* パラメータセット */
                }
                break;
            case EnemyState.SHOT:
                _animator.SetTrigger("gun");
                var vector = (_player.transform.position - transform.position).normalized;
                _enemyFactory.CreateBullet(_spriteRenderer.flipX,this.transform.position, vector);
                _enemyState = EnemyState.ATTACK_WAIT;
                /* パラメータセット */
                break;
            case EnemyState.ATTACK_WAIT:
                if (_countdownTimer > 0)
                {
                    _countdownTimer -= _countdownAfter * Time.deltaTime;
                }
                else
                {
                    _enemyState = EnemyState.SEARCH;
                }
                break;
        }
SEARCH

プレイヤーの位置を取得して、若干ランダムで位置をずらして保持します。
その後PATROLに遷移

PATROL

SEARCHで取得した位置に移動します。
移動速度はStageパラメータで設定します。
目標地点に到達したらATTACK_BEFOREに入ります。

ATTACK_BEFORE

攻撃前の待機です。構えと言い換えてもいいです。
10秒のカウントダウンを行い、SHOTに遷移します。
このカウントダウン速度もパラメータで設定。
この時プレイヤーが離れすぎた場合は再度SEARCHに遷移します。

SHOT

弾を発射します。animatorに合わせて微調整入れてます。

ATTACK_WAIT

弾発射後の硬直です。
ここもパラメータ
硬直が解けたら、再度SEARCHに遷移して、プレイヤー位置を上書きします。

その他

後は普通にColliderで当たり判定を作っていたり
画面遷移、_animatorをストップさせて会話パートをゲーム中に挟んでみたりと
といった細かい実装なので割愛。

終わってみて

企画、システム(ゲーム)、デザイン
をわずか一週間で形にする…。これは大変な事です。
ちょっと無謀だったな…というチャレンジでしたが、
なんとか遊べるレベルにまで持っていけてよかったです。
そして評価、コメントまで頂けて本当に嬉しいです。

余談

作業中はよく適当にアニメを流しているんですが、今回は
ガールズアンドパンツァーをTV、OVA、劇場版、最終章と続けて二周ぐらい流してました。
次は戦車にしようかなw

【Unity】タッチした位置にuGUI(RectTransform)を表示する

お疲れ様です。ぐーるです。
最近はなんかサーバだけではなく、採用だ評価だと色んな所まで見てたりして、
ちょっとアップアップしています。色んな人と会話するのって難しいです。

unityroom以外でもなんかゲーム作ってて、ふと
「指定位置にuGUIでテキストと画像を出したいなー」
と思ったので、実装したのですが、なんか思ったように上手く行かずにちょっと困ったので
メモ代わりにまとめておきます。

タッチした位置にuGUI(RectTransform)を表示する

普通のGameObjectだったらScreenToWorldPoint()とか使いますよね。
こんな感じ。

f:id:ghoul_life:20181113000239g:plain

public GameObject cubePrefab;
	
// Update is called once per frame
void Update () {
        if (Input.GetMouseButtonDown(0))
        {
            var mousePosition = Input.mousePosition;
            mousePosition.z = 10;
            var pos = Camera.main.ScreenToWorldPoint(mousePosition);
            var cube = Instantiate(cubePrefab);
            cube.transform.position = pos;
            cube.transform.SetParent(this.transform);
        }
}

が、RectTransformではどうするんだろうと言うとこうします。
タッチした位置にInstantiateしてDoTweenでアニメーション付与して終わったら消す。

f:id:ghoul_life:20181112235926g:plain

// uGUIのprefab
public GameObject itemPrefab;

// Update is called once per frame
void Update () {
        if (Input.GetMouseButtonDown(0))
        {
            var canvas = this.GetComponent<Canvas>();
            var canvasRect = canvas.GetComponent<RectTransform>();

            Vector2 localpoint;
            RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, Input.mousePosition, canvas.worldCamera, out localpoint);
            var item = Instantiate(itemPrefab);
            item.transform.SetParent(this.transform);
            item.GetComponent<RectTransform>().anchoredPosition = localpoint;
            DoTweenUtil.UpToRectTransform(item);
        }
}

補足でDoTweenのコードも

    public static void UpToRectTransform(GameObject gameObject)
    {
        var rectTran = gameObject.GetComponent<RectTransform>();
        if(rectTran != null)
        {
            rectTran.DOMove(new Vector2(0 , 50) , 1.0f)
            .SetRelative(true)
            .OnComplete(() => {
                UnityEngine.Object.Destroy(gameObject);
            })
            .SetEase(Ease.OutCubic)
            .Play();
        }
    }

こんだけだけど

なるほど、RectTransformUtilityなんて便利なものがあるのね。
また一つ勉強になりました。

もうすぐunity1weekが始まりますね。お題は「クリスマス」ではないかと思っているのですが、
そんなことないか。
新型iPad ProとApple Pencilを衝動買いしてしまったのでお絵かき練習だけは継続しています。

f:id:ghoul_life:20181113000707p:plain

まだまだですね…。

【Unity】いい感じにデータクラスをList表示したい【Inspector】

なんか連投です。
きっとこの後間が空きます!
時間とやる気があるときにやっとけ!って事ですね。

リスト表示したい

なんか簡単なマスタデータ的なデータ持ちたくて
でもcsvにするほどでも無くて、でも折角Unity使ってるんだからオシャレにリスト表示したい。

f:id:ghoul_life:20181029235212p:plain

こんな感じにInspectorを活用したい。
調べればすぐ出てくるかもしれないけど、自分でもメモっておく。

PropertyDrawerとReordableList

ソースから。Dataクラスをリスト表示する最小限な構成を紹介

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable] // <- これが大事。忘れずに
public class Data {
    public int _id;
    public string _name;
    public DataList.DataActionType _actionType;
}
using UnityEngine;

// GameObjectに付けるのはこれ。
public class DataList : MonoBehaviour
{
    public enum DataActionType
    {
        NONE,
        TYPE1,
        TYPE2,
        TYPE3
    }
    [SerializeField] Data[] _dataList;
}

以下はEditor拡張だ。

// Project/Editorの下に配置でもOK
#if UNITY_EDITOR

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(Data))] // DataクラスのInspector表示をカスタム
public class DataPropertyDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 表示幅
        float[] widthes = { position.width * 0.2f, position.width * 0.5f, position.width * 0.3f };

        if (property != null)
        {
            position.width = widthes[0];
            EditorGUI.PropertyField(position, property.FindPropertyRelative("_id"), GUIContent.none); // フィールド名を指定

            position.x += position.width;
            position.width = widthes[1];
            EditorGUI.PropertyField(position, property.FindPropertyRelative("_name"), GUIContent.none);

            position.x += position.width;
            position.width = widthes[2];
            EditorGUI.PropertyField(position, property.FindPropertyRelative("_actionType"), GUIContent.none);
        }
    }
}
#endif
// Project/Editorの下に配置でもOK
#if UNITY_EDITOR

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

[CustomEditor(typeof(DataList))]
public class DataListEditor : Editor
{
    private ReorderableList _reorderableList; // ReorderableListを利用して、並び替えや+-ボタンを使えるようにする

    void OnEnable()
    {
        _reorderableList = new ReorderableList(serializedObject, serializedObject.FindProperty("_dataList"));
        _reorderableList.drawElementCallback += (Rect rect, int index, bool selected, bool focused) =>
        {
            SerializedProperty property = _reorderableList.serializedProperty.GetArrayElementAtIndex(index);
            // PropertyFieldを使ってよしなにプロパティの描画を行う(PropertyDrawerを使っているのでそちらに移譲されます)
            EditorGUI.PropertyField(rect, property, GUIContent.none);
        };
        _reorderableList.drawHeaderCallback += rect =>
        {
            EditorGUI.LabelField(rect, "id | name | action type");
        };
    }


    public override void OnInspectorGUI()
    {
        _reorderableList.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
}
#endif

これだけでOK

上記のコードだけで画像のようなリスト表示が出来るようになる。便利~。

【Unity】【Android Plugin】android連携で画像を読み込みたかっただけなのにちょっとハマった話

お疲れ様です。ぐーるです。
最近もお絵描きの練習と新しいゲームの開発をコツコツやってます。
もうすぐ1weekまた始まるんでしたっけ。
ヤバい。Inventoryシステムの完成を急ぎたい。

Androidプラグイン

UnityからAndroidネイティブの機能を使いたい場合はプラグインを作成する必要があります。
作り方についてはちょっと調べると沢山出てくるのでここでは割愛。
プロジェクト作って、
新しくモジュール追加でLibrary選んで、
適当にコード書いてビルドして、
aarからjar取り出すかbundlesの下から取ってきて、
Plugins/Androidの下に配置すればOKってな具合です。

まぁ大概はビルド&配置task作って楽しますかね。

画像が出ない?

画像を読み出す時はこんな感じのコードを書きます。

// res/drawable/hogehoge.pngを読み出す
Context unityContext = UnityPlayer.currentActivity.getApplicationContext();
int resourceId = unityContext.getResources().getIdentifier("hogehoge", "drawable", unityContext.getPackageName());

ふむふむ、なるほど。ではこのまま書こう。
と書いてみるとhogehoge.pngが読めない。
えー何故だ!?Plugins/Android/res/drawableの下にちゃんと置いてるよ?と。
今回はこのお話。

結論

ハッキリ言ってしまえば配置ミスです。
こう配置する必要がありました。

// Plugins/Android/の下
res/drawable-hdpi-v4/
res/drawable-ldpi-v4/
res/drawable-mdpi-v4/
res/drawable-xhdpi-v4/
res/drawable-xxhdpi-v4/
res/drawable-xxxhdpi-v4/

正解は全部作って全部にちゃんとhogehoge.png入れとけ!です。これだけ。

出来上がったjarかAndroid exportしたAndroidプロジェクトを見てみればすぐわかりますが、
res/drawableの下に置いたビルド後に画像が無くなってるんですよね。
恐らくUnity側でそこは自動でアイコンを配置したりなどで使ってて
何か入れておいても無視されてる様子。

Androidやってる人からしたら常識なのかもしれないですが、
dpiによってリソース分けられてるんですよね。すっかり忘れてました。

余談

アイコン読み出したいときはこう書きます

Context unityContext = UnityPlayer.currentActivity.getApplicationContext();
// iconを読み出す
PackageManager packageManager = unityContext .getPackageManager();
ApplicationInfo applicationInfo = null;
try
{
    applicationInfo = packageManager.getApplicationInfo(unityContext .getPackageName(), PackageManager.GET_META_DATA);
}
catch (PackageManager.NameNotFoundException e)
{
    e.printStackTrace();
    return;
}
appIconResId = applicationInfo.icon; // iconのリソースIDを取り出す

【Unity1week】一週間ゲームジャム「あつい」に参加【Unity】

お疲れ様です。お久しぶりです、ぐーるです。
仕事の方ではゲームとは関係ないまま、すっかり中堅、それ以上となり
打合せ、社外MTGなど座席に余り居ないような毎日を送っています。
Unityの新規開発プロジェクトとかやりたいです。

前回からまた長い間が空いてやってきました、unity1week。
自分からするとつい先日宴ゲームジャムがあったので、
そこまで間が無かったようなそんな印象なんですが、やっぱりこっちも参加したいと。

ghoul-life.hatenablog.com


そうでもしないとunityを触る機会とか無いんだものー

お題について

これまた難しい。どうしようかウンウン唸ってみるが全然アイデアは降りてこない。
・おでん?
・太陽?
・お湯?
こういう時にマインドマップとかやるといいのだろうけど、
流れに身を任せて日々を過ごす。

とりあえず漠然と考えていたのは
「自分でキャラなにか描きたいなぁ」
だった。

相変わらず下手だが、下手の横好き、別に見られても減るもんじゃないし!
(でもリアル知り合いには言わないし、見せないw)
という気持ちで、
自分でも描けそうで、
作りやすそうで、
カジュアルで、
工数は2日ぐらいないい感じの無いかなーと案を巡らせていると
よくある正弦波とかを利用した波乗りとかどうだろうとふと思いついた。

なみのり

昔メッシュを利用したマップを作ってその上を滑らせたりしてたんで
これならすぐ出来るなーと思った。ひっくり返ったらゲームオーバーにしようと。
とりあえず枠組みはすぐ出来るし、キャラも波乗りしている女の子でいいじゃんと。

サーフィンやボディボードとかも思ったけどわかりやすく浮き輪にしました。
浮き輪なら遅くても平気だし!
と描き始める。

f:id:ghoul_life:20180917142613j:plain

ざっとこんな感じで座ってて〜髪は長めで〜
とか試行錯誤をやってるうちにどんどん深みにハマっていく。(ここがめっちゃ楽しいんだけど)
「他のバリエーションとか浮き輪も乗るタイプのものとかアニメーションとか付けたいなー」
と思っているうちに期限は刻一刻と迫っていた。

ゲームについて

もうこっちは特に言うこと無いですね。
uvスクロールで水面を動かしつつとplaneを利用してスクリプトからmesh変形させてるだけ。

    private void Wave()
    {
        _mesh = this.GetComponent<MeshFilter>().mesh; // planeのメッシュ
        _meshCollider = this.GetComponent<MeshCollider>();
        _verticies = _mesh.vertices;

        int counter = 0;
        int yLevel = 0;

        for (var i = 0; i < iLen; i++) 
        {
            for (var j = 0; j < jLen; j++)
            {
                Calc(counter, yLevel);
                counter++;
            }
            yLevel++;
        }

        _mesh.vertices = _verticies;
        _mesh.RecalculateBounds();
        _mesh.RecalculateNormals();
        

        _meshCollider.sharedMesh = _mesh;

    }

    private void Calc(int i , int j)
    {
            var x = (_verticies[i].x + this.transform.position.x) / _detailScale;
            var y = (_verticies[i].y + this.transform.position.y) / _detailScale;
            _verticies[i].z = Mathf.PerlinNoise(x, y) * _heightScale;
            _verticies[i].z -= j;
    }

これプレイが進むと自動でパラメータが切り替わっていくんだけど
この切り替わりがちょっと唐突なのが微妙だった。
今後の課題だなと。lerpで向かっていくといいのかな。

(実はこんなトリッキーな波にすることとかも出来るのです)

f:id:ghoul_life:20180917164252p:plain

絵について

結局描けたのは
・通常
・ジャンプ時
・落ちた時
の3つだけ。
あとUIパーツとかそういったものも手描きで作ってたりしてます。

次回について

次ももちろん参加します。

それとは別で次のゲームの構想は決まっていて
作り出していきたいのですが仕事が忙しい…。
平日は10-23が標準で日付跨ぐ、朝までコースもしばしば。
これで家族がいる人とかどうやって暮らしてんだろう?
とか全然わかりませんね。

あ、宴ゲームジャム、賞ほんとにありがとうございました。
ADVの話のアイデアも練っておこう!
「Unity & 宴ノベルゲーム開発入門」出版記念オンラインゲームジャム | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう

【Unity】「Unity & 宴ノベルゲーム開発入門」出版記念オンラインゲームジャム に参加しました

https://unityroom.com/games/shirakisou

先日、ゲームジャムへの提出が完了した。
今回は初のADVということで、思ったこととか大変だったことでも書いてみようかなと。
あんまり技術的な事はありません。基本は全て宴任せ。

ADVについての想い

昔(SFC時代)はかまいたちの夜とかそういったサウンドノベルっていうジャンルが流行った事がありました。

時代は流れ、逆転裁判ダンガンロンパといったカジュアルなものがヒットしたり、
シュタインズゲートが大ヒットしてアニメ化映画化までして、長期ヒットを続けていたり
四十八(仮)がある意味有名になったりと、緩やかな進化を続けているものの、
大きくは変わらず、基本は文章力で勝負しているジャンルになるのかなと思います。

www.famitsu.com
こんなインタビューとかもありました。

それに挑戦しようと言うのは無謀な感じがしましたが、
やるだけやってみようと言うわけでまずは適当なメモ書きから始めました。

メモ書き

・殺人事件を題材にする

容疑者は三人
- 友人のおじさん
- 恋人の女性
- 仕えていたメイド

死因は背中から刺殺
ナイフは死体に刺さったまま
部屋は施錠されていた

キッチンにいた
居間にいた
部屋に居た
でアリバイはなし

誰に話しかけよう
・三人
・もう充分(犯人当てに)

3つの質問
・殺された人
・その他1
・その他2

おじさん
・十年来の友人だった
お金を借りていた事もあったが、今はもう無い

・最近付き合いだしたと聞いた
今回が初対面

・三年ほど前からいるメイドだ
ほとんど会話したことがない

当初はこんな感じでした。
思いついた設定や状況などをとりあえずメモる。
そして繋いでいこうとしてました。

エンディングを先に作る

誰かが言っていた。ゲームはエンディングから作れと。
そうしないといつまで経っても終わらないと。

なので、犯人をざっくり決めて、エンディングの結びの言葉まで
先に決めてました。
これが無かったら絶対間に合ってなかった…。
ありがとうセンパイ。

どんどん間延びする文章

OP -> 到着 -> 環境 -> 人物紹介 -> 事件 -> 調査 -> 解決 -> ED

当初はこんな流れにしようと決めていました。
「二日目とか無いと短すぎるかなー」
とか余裕ぶっこいてたらとんでもなかった。

とりあえず書く
->
読み直す
->
違和感を直す
->

のループで精度を上げていて
「ここ唐突だな」
と思ったところを直し続けていると、
あっという間に10行、20行と増える。

気がついたら三万文字を超えていた。

「あ、こりゃダメだ」
と思い切って組み直しをすることに。

思い切って

要点だけに絞っていく作業。
だが、最小限の人物紹介は必要だなとその辺りも組み直し。
最初にミステリーですよ!ということを伝えるために殺人現場からのスタートにした。

事件発覚 -> 人物紹介 -> 事件 -> 解決 -> ED

とシンプルな座組に。

もう少しゲームらしくするには、マップを移動することが出来たり、
質問する内容を自分で選択出来たりといった部分を作れば良かったのですが、
どうしてもプレイ時間が間延びしてしまってそれは断念しました。

個別に会話をして人となりを知るシーンなどもあったのですが、泣く泣く外しました。

宴について

技術的な事は無いとか言っちゃったけど、ちょっとだけ。
とにかく言えることは

「困ったらSample.xlsを見ろ」

これに尽きる。これに全てが乗っていた。
ですが、とりあえず自分が使えるようになるために要点に絞って調べたことを。

xlsのシート「Start」から始まる
分岐:Selection
ラベルに飛ぶ:Jump
ローカルラベル:**~~~
シート名:*~~~
キャラの表示:Character , CharacterOff
頻出しそうなコマンドをメモ
- FadeIn , FadeOut : フェードイン、フェードアウト。 Arg6で秒数を指定出来る
- Wait : 待機。Arg6で秒数指定
- bg : Arg1で指定した背景に差し替え
- Sprite : 背景の上に出す画像を指定
- SpriteOff : 差し込み画像を削除
- Bgm , Se のサウンド

と使いたい機能だけに絞って少しずつ把握していった。
特にLayerは非常に強力なので、これは使いこなしたいし、
自分でも作れるようになりたい。

感想

製作期間は1week challengeと同じ一週間です。
最初は本当にボリューミーでした。

文章を書く、ということが国語の授業とか以外では本当に初めてだったのですが
思ったよりも書くの楽しかったなーと。また書きたい。
ですが、文章を書くだけなら簡単なんですが、まとめるのは本当に難しかったです。

そして最後に、EDはちょっと切なげなEDになってます。
良ければちょっとプレイしてもらえると、そしてハートを押して下さいー!

推理ADV 白木荘の殺人 | 無料ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう