さくらの作業ログ

今度は三日坊主にしないといいな

さくらの作業配信

平日2時間くらい勉強したいけどついついごろごろして時間を溶かしてしまうので誰かに監視してもらおうという趣旨で配信をやっています! 平日2時間くらい勉強したいけどついついごろごろして時間を溶かしてしまう仲間、おいで!一緒に作業しよう!

www.twitch.tv

Unityもくもく会平日20時から!
Unityもくもく会平日20時から!

※やってないときは残業してると思ってください

Blenderで作ったアニメーションをUnityで再生する

Blenderでアニメーションを作成する

アニメーションの作り方に関してはブログとか動画とかいくらでもあるんでそっちを参照してください。
今回は関節も何もない生き物(?)を作ったので本当に話すことが何もありません。すべてノリと勘です

作成したものをアクションシート(上図)の「ストリップ化」で保存します。ストリップって何?

NLAエディター」や「ビデオシーケンサー」でアニメーションを扱う単位の1つ。オフセットにより、本来の開始や終了範囲からずらして再生を始めたり、分割やカットによるストリップ自体の加工、他のストリップとのブレンドが可能。

引用元: 窓の森 forest.watch.impress.co.jp

なるほどなにもわからん
とりあえずこれをやらないとソフト閉じたときに作ったアニメーションが消えるらしい。
Unity側でアニメータ設定するときにもここの名称を使うので、わかりやすい名前を付けておいてください。

作ったモデルをUnityに読み込む

ファイル→エクスポート→fbxからfbxファイルをエクスポートします。

「アニメーションをベイク」にチェックを入れてください。
あと個人的なおすすめは内容の中の「可視オブジェクト」にチェックを入れることです。没パーツを消し忘れていると想定とだいぶ違うものが出てくることがあるので(没パーツをちゃんと消せ)

吐いたfbxファイルをUnityに投げ込みます。ドラッグ&ドロップでOKです。

そうするとなんかいい感じにアニメーションのファイルとかも入ってきます。ファイル?設定?
ループアニメーションとかはUnity側でも設定をしないとループにならないので、インスペクタの下の方にある設定をわちゃわちゃします。

アニメーター設定

プロジェクトパネルで右クリック→作成→アニメータコントローラを作成します。
正しいのかよくわかりませんが、現状こうしています。

被ダメモーションを再生するために、Damagedというフラグを作っています。
これがtrueのときにダメージモーションが再生されます。

コードはこう

public async UniTask Damaged(){
    anim.SetBool("Damaged", true);
    await UniTask.DelayFrame(25);
    anim.SetBool("Damaged", false);
}

ダメージモーションが25フレームなので、Damagedをtrueにしたあと25フレーム待ってfalseに戻す形式です。
ループアニメーションにしてないので繰り返し再生されることはない気がするけど、ここでfalseにしておかないと次の被ダメでおかしくなる。たぶん。

で結果がこう

攻撃モーションが再生されている気がする。

アニメーションの同時再生はできない

BlenderNLAみたいにホバリング(羽ばたき)モーションと被ダメモーションとかを同時に再生できないかな~と思ったものの、できないっぽい。
アニメーション修正ですね~

宣伝

ニコニコに動画も上げてるからよろしくね~

行動選択パネルをひとつひとつキャンセルしたい

イメージ図

前回の状態がこう

あと今の作りだと、スキルを選んでからやっぱり攻撃にしようとか、そういう戻り方ができないっぽい。

だったところから、とりあえず戻れるようにはしました。しかし、「スキルを選んでから→やっぱり攻撃にしよう」の戻り方はできましたが、「スキルを選んで→使うスキルも選んでから→やっぱり攻撃にしよう」になるとうまくいきません。

    public async UniTask<Action> AwaitAnyButtonClickOrCancelAsync(CancellationToken cancellationToken)
    {
        m_cancellationTokenSource = new CancellationTokenSource();
        playerCancellationToken = m_cancellationTokenSource.Token;

        CancellationTokenSource linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken, playerCancellationToken);

        var (index, result1) = await UniTask.WhenAny(AwaitAnyButtonClickedAsync(linkedTokenSource.Token), waitKeyDown());

        Debug.Log($"index: {index}");
        Debug.Log($"result1: {result1}");

        return new Action();
    }

引数として渡ってくる cancellationToken はゲームマネージャの this.GetCancellationTokenOnDestroy() の値です。これにプレイヤー入力監視用の playerCancellationToken をくっつけて次に渡しています。

理想形

現在の状態

CancellationTokenを使いまわしているのでそれはそうなんですが、これどうしたらいいのかわかりません。

このキャンセル処理がこうなんですが

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            Debug.Log(GetType().Name);
            m_cancellationTokenSource?.Cancel();
            m_cancellationTokenSource = null;
        }
    }

これがスキルを選択するパネルと効果対象を選択するパネルはともかくほかのパネルも全部反応する。disabledになっているものまで全部反応する。なんでや!!!!!!!!
disableになってるものが反応するのはさすがに納得いかない。disableにしただけで非表示になるのも納得できないのに(# ゚Д゚)

全部のパネルでこの Input.GetKeyDown(KeyCode.Escape) が発火してしまうので全部キャンセルされてしまっている様子。

cancellationTokenSourceを破棄してみたり作り直してみたりもしたんですが、挙動変わらず。これでも操作できるといえばできるけど、UIの挙動としてはいまいち。やっぱり一個ずつキャンセルしたい。

現在の状態は以下 github.com

MagicToDoでタスクを作った

話題になってたAIがタスクを作ってくれるツール MagicToDo でタスクを作成した。

MagicToDo

作成されたタスク

想像以上に妥当なタスクが出てきて結構びっくりした。
そして二年かかるって言われてそれもびっくりした。今年の目標なんですけど……
とはいえまあ、見積もりに関してはそんなに信用しなくてもよいでしょう。教師データ貧弱そうだし。もう一回やり直したら一年と百日とか言われたし。キャラデザ14日で終わる気しないし。
敵キャラクターとかちゃんと作ろうとしたら世界観に合わせてデザイン作って3D起こして待機モーションと攻撃モーションと被ダメモーションを作りたいわけで、ちょっと気が遠くなるもんな。

宣伝

昨日ニコニコに動画を上げてるのでそっちもよろしくお願いします。

前回投稿分に「楽しみにしてるよ」ってコメントついてて嬉しくてじたばたしてしまった。
再生数とかは自動再生で入ってくる人もいそうなのであんまり真に受けないようにしてるんだけど、わざわざコメント付けてくれた人がいるなら、それは真に受けてもよかろう。めちゃめちゃ嬉しい、ありがとうありがとう。どこの誰かわからないけど、あなたに遊んでもらうためだけにでも頑張れる気がしてきたよ。

全然中途半端だけどいつまでもキリが良くなりそうにないので記事を書いておきたい

バトル機能を作成中です。

ドラクエ風コマンドバトルです
動作イメージ
イメージはドラクエとかのコマンドバトルで、UIのデザインは未定です。

    private async UniTask<List<Action>> PlayerAction()
    {
        List<Action> actions = new List<Action>();

        for(int i = 0; i < allies.Length; i++){
            var who = allies[i];
            actions.Add(await SelectAction(cancellationToken));
        }

        return actions;
    }
    private async UniTask<Action> SelectAction(CancellationToken cancellationToken){
        selectActionPanel.enabled = true;
        SelectActionPanel selectActionPanelComponent = selectActionPanel.GetComponent<SelectActionPanel>();
        int actionGroup =  await selectActionPanelComponent.AwaitAnyButtonClikedAsync(cancellationToken);
        Action action = new Action();
        switch(actionGroup){
            case 0:
                action = await SelectAttackTarget(cancellationToken);
                // var target = await SelectAttackTarget(cancellationToken);
                // return calculateAttackEffect();
                break;
            case 1:
                action = await SelectSkill(cancellationToken);
                // var target = await SelectSkillTarget(cancellationToken);
                // return calculateSkillEffect();
                break;
            case 2:
                var act = await SelectItem(cancellationToken);
                // var target = await SelectItemTarget(cancellationToken);
                // return calculateItemEffect();
                break;
            case 3:
                var target = await SelectDefenceTarget(cancellationToken);
                // return calculateDefenceEffect();
                break;
        }
        selectActionPanel.enabled = false;
        return action;
    }
    private async UniTask<Action> SelectSkill(CancellationToken cancellationToken)
    {
        skillPanel.enabled = true;
        SkillPanel skillPanelComponent = skillPanel.GetComponent<SkillPanel>();
        skillPanelComponent.allySelectPanel = selectTargetAllyPanel;
        SelectTargetAllyPanel selectTargetAllyPanelComponent = selectTargetAllyPanel.GetComponent<SelectTargetAllyPanel>();
        selectTargetAllyPanelComponent.setAllies(allies);
        
        skillPanelComponent.enemySelectPanel = selectTargetEnemyPanel;
        SelectTargetEnemyPanel selectTargetEnemyPanelComponent = selectTargetEnemyPanel.GetComponent<SelectTargetEnemyPanel>();
        selectTargetEnemyPanelComponent.setEnemies(enemies);
        Action act = await skillPanelComponent.AwaitAnyButtonClikedAsync(cancellationToken);
        skillPanel.enabled = false;
        return act;
    }

まず「攻撃、スキル、アイテム、防御」の四択のパネルが出る
→選択したものに合わせた次のパネルが出る(攻撃を選んだらターゲット選択、スキルを選んだらスキル選択)
→決定したらすべてのパネルを一括で閉じる

という形状にしたいので、await処理の入れ子にしています。スキルはスキルを選択してから更に対象を選択(攻撃スキルなら敵、回復スキルなら見方から対象を選択する)があるのでややこしい

今のところとりあえず表示ができればいいや~で書いてるんですが、この書き方だとターンごとにプレハブ生成しちゃってて非効率なのでそこは修正。敵味方は減る可能性があるので更新の必要があるけど、スキル自体は増えないし減らないので一旦更新なし?MPが尽きたときにどうするか未定。

そのあたりの表示の更新は敵味方の攻撃でHPとMPを減らしたり敵味方自体を減らしたりする処理が必要なので、次は残HPとMPの計算かしら。
その前にCancellationTokenを何とかした方がいいかもなー。勝つか負けるかするまで無限ループでターンを回そうとしたんですが、中身まともに作ってないので再生ボタンを押したら完全フリーズしてしまったので。
あと今の作りだと、スキルを選んでからやっぱり攻撃にしようとか、そういう戻り方ができないっぽい。

2024年ことはじめ

2024年、たまには気合を入れて新年を始めようと思ってウィッシュリストなるものを制作しました。

ゲームの一部分を公開する

昨年の末頃、作っていたゲームからバトル機能だけを分離しました。
unitypackage化を試したかったのと、再利用性を高めるためです。今のところあまり再利用性は上がっていない気がしますが。

unityroomというサイトの存在を知ったので、ここにこのバトル機能をデプロイしたいねというのが今年の目標になります。
イメージはグラブルのトライアルバトルで、ゲーム開始→パーティ選択(未定)→戦闘→リザルト画面→ゲーム開始に戻る、という構成。
この構成だけだと経験値の獲得とかアイテムのドロップとかは必要ないことになりますが、どうしようかな。どのタイミングで実装しようかちょっと悩みどころです。

下位の目標として

  • BGM作成
  • 敵デザイン6体くらい(想定している属性が5つなので無属性合わせて6種類)
  • 味方デザイン4人~ 戦闘ゲームとしては選択肢があった方がいいのでもうちょっと増やしたい
    • 戦士
    • ヒーラー
    • タンク
    • 魔法使い
    • その他(あんまり考えていない)
      みたいなところから4人くらいチームを編成して遊べる感じ
  • スキル作成
  • 武器作成
  • UIボタンデザイン
  • UIテキスト出すとこデザイン リザルト画面とかのシステムメッセージのやつ
  • バトル設定画面 キャラクターを選択してパーティを作成する
  • スプラッシュ画面 あったほうがよかろう
  • バトル結果画面

ウィッシュリストを作っていたはずなのに、達成条件をちゃんと考えてリストアップしたら単なるToDoリストになってしまった。ウィッシュリストとは? と思って人のリストを見に行ったらもっとふわっとしていて楽しい感じというか、こうなればいいな~みたいなものだった。うーん不向き

仕事

  • 見積もりの精度を上げる 作業量を調整するためには見積もりの精度が必要なんだけど見積もり精度自体の評価基準が無いのでどうやって評価しようか考えねばならない
  • 仕事を調整できるようになる ほぼ上と同じ。見積もりの精度が上がれば期限と残作業とすり合わせて調整しやすくなるかな~とぼんやり考えている

一番具体案が必要な仕事の目標が一番ふんわりしている

月次

  • 動画出す

動画投稿を主眼に置くならもっと頻度を上げた方がいいんだけど、動画作るためにゲーム制作の時間を削りたくないので月次くらいにしておく

週次

  • ブログ書く

週次目標。書くことが無ければ週一投稿にこだわる必要は無いけど、年50本書けたらいいよね~と思ってはいる。

日次

  • 早起き
  • 散歩
  • ストレッチ

早寝は去年の段階でポケモンスリープが達成させてくれたのでなし。起きるのは自力なので達成できていない

日次目標は体調不良の分とか鑑みて年8割達成くらいで完了にしていいかな~の気持ち。とはいえ昨年は夏の間全然散歩しなかったので(気温高すぎて散歩なんかしてたら死ぬと感じたため)8割もちょっと無理かもな~

ウィッシュリスト100件上げる動画を見ながらやったけど100件は思いつかなかった。思いついたら追記するかもしれない(たぶんしない)

初期設定画面を作った

初期設定の動作イメージ図
動作イメージ

長いんですがコードがこう

public class InitialSceneController : MonoBehaviour
{
    public SaveData saveData;
    public GameObject SelectLanguage;
    public GameObject InputName;
    public GameObject SelectMode;
    public Canvas LoadingOverlay;
    private SaveManager saveManager;
    private string targetSceneName = "FieldScene";

    async void Start(){
        saveManager = new SaveManager(saveData);

        VisibleLanguageSelect(false);
        VisibleInputName(false);
        VisibleModeSelect(false);
        LoadingOverlay.enabled = false;

        await PlayerInitialSettings();

        LoadingOverlay.enabled = true;
        InitialSaveData initialSaveData = new InitialSaveData(saveData);

        await initialSaveData.createAsync(saveData.selectedMode);
        saveManager.Save();

        Complete();
    }

    public void VisibleLanguageSelect(bool visible){
        SelectLanguage.transform.parent.gameObject.GetComponent<Canvas>().enabled = visible;
        SelectLanguage.GetComponent<LocaleSelector>().enabled = visible;
    }

    public void VisibleInputName(bool visible){
        InputName.transform.parent.gameObject.GetComponent<Canvas>().enabled = visible;
        InputName.GetComponent<PlayerNameSettings>().enabled = visible;
    }

    public void VisibleModeSelect(bool visible){
        SelectMode.transform.parent.gameObject.GetComponent<Canvas>().enabled = visible;
        SelectMode.GetComponent<ModeSelector>().enabled = visible;
    }

    public async UniTask PlayerInitialSettings(){
        saveData.selectedLocale = await PlayerLanguageSetting();
        saveData.playerName = await PlayerNameSetting();
        saveData.selectedMode = await PlayerPlayModeSetting();
    }

    private async UniTask<string> PlayerLanguageSetting(){
        VisibleLanguageSelect(true);
        LocaleSelector localeSelector = SelectLanguage.GetComponent<LocaleSelector>();
        string language = await localeSelector.selectAsync();
        VisibleLanguageSelect(false);
        Debug.Log(language);
        return language;
    }

    private async UniTask<string> PlayerNameSetting(){
        VisibleInputName(true);
        PlayerNameSettings playerNameSettings = InputName.GetComponent<PlayerNameSettings>();
        string playerName = await playerNameSettings.DecideNameAsync();
        VisibleInputName(false);
        Debug.Log(playerName);
        return playerName;
    }

    private async UniTask<string> PlayerPlayModeSetting(){
        VisibleModeSelect(true);
        ModeSelector selector = SelectMode.GetComponent<ModeSelector>();
        string selectedMode = await selector.selectAsync();
        VisibleModeSelect(false);
        Debug.Log(selectedMode);
        return selectedMode;
    }

    // すべての設定を終えたら遷移
    void Complete()
    {
        SceneManager.LoadScene(targetSceneName);
    }
}

個別に説明していきます。

設定用のCanvasをすべて非表示にする

       VisibleLanguageSelect(false);
        VisibleInputName(false);
        VisibleModeSelect(false);
   public void VisibleLanguageSelect(bool visible){
        SelectLanguage.transform.parent.gameObject.GetComponent<Canvas>().enabled = visible;
        SelectLanguage.GetComponent<LocaleSelector>().enabled = visible;
    }

最初は Canvas.enabled だけするようになってたんですが、KeyDownイベントが全部のスクリプトで発火したのでスクリプトもdisabledにするようになっています。

設定用のCanvasを順番に表示する

   public async UniTask PlayerInitialSettings(){
        saveData.selectedLocale = await PlayerLanguageSetting();
        saveData.playerName = await PlayerNameSetting();
        saveData.selectedMode = await PlayerPlayModeSetting();
    }

ユーザーの決定を待つためにUniTaskを使ってawaitすることにしました。
呼ばれる側のスクリプトはこうとか

   // ボタンが選択された時に実行
    public async UniTask<string> DecideNameAsync()
    {
        await buttonEvent.OnInvokeAsync();
        return inputName.GetComponent<TMP_InputField>().text;
    }

こうとか

   public async UniTask<string> selectAsync(){

        await UniTask.WhenAny(waitKeyDown(), waitMouseClick());

        Toggle selected = this.gameObject.GetComponent<ToggleGroup>().ActiveToggles().First<Toggle>();
        string value = selected.GetComponent<ButtonWithValue>().value;

        return value;
    }

await UniTask.WhenAny(waitKeyDown(), waitMouseClick()); のところはキー入力かマウスクリックを待機するというコードで、マウスクリックの検知は今まだ無いんですけど、キー入力の方を例に挙げておきます。

   public UniTask waitKeyDown(){
        return UniTask.WaitUntil(() => Input.GetKeyDown(KeyCode.Return));
    }

ラムダ式」という言葉が思い出せなくてしばらく呻いてしまった。Javaラムダ式が導入された時期くらいからJava触らなくなったので記憶が遠かった…
これと似たような感じでマウスクリックの判定も作りたいんだけど、これがどうしてうまくいかない。難しいねー。
イメージとしては「矢印キーで選択してエンターで決定」もしくは「マウスクリックで決定」という感じなんですが、素のままだとマウスクリックも選択なのよね。マウス前提ならボタンの方がいい、キーボード前提ならトグルの方がいい。たぶん。ここを共存させたいんだけどなー。

設定が終わったらセーブデータを作成する

       LoadingOverlay.enabled = true;
        InitialSaveData initialSaveData = new InitialSaveData(saveData);

        await initialSaveData.createAsync(saveData.selectedMode);
        saveManager.Save();

ここもawaitしています。今のところデータが無いので動作はサクッと終わりますが、今後フラグが増えたときにどうなるかはよくわからないため、ローディングオーバーレイ(Web屋の言い方かもしれない、スピナーとかプログレスバーとか表示するアレ)を表示するようになっています。

InitialSaveDataの中身はまだあんまり考えていないのでこうとか(最強モード)

   private async Task<bool> createStrongModeData(){
        // 注意:特に非同期処理をやっていないので黄色エラーが出ているコードです
        Debug.Log("createStrongModeData");
        saveData.selectedMode = "strongmode";
        saveData.attackPower = 999;
        saveData.defensePower = 999;
        return true;
    }

そもそも「最強モード」っていうのが既に超バカっぽい。バカっぽいもの大好きですが

簡単なユーザー登録画面くらいの気持ちで組んでたのにスレッドの概念を完全に忘れてて結構時間かかっちゃった。
でもまあいいリハビリになったかなー。

ソースはここから
github.com

いまだによくわかっていないんだけど、 *.meta ってコミットする必要あるのか?