さくらの作業ログ

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

バトル機能のリファクタリングをした

ご無沙汰してます。
仕事に忙殺されたり上司と喧嘩したり転職したりしていました。

途中モデリング記事とか書いていたようですが(記憶にない)だいたい前回の話は以下
戦闘画面のUIを作る

とりあえずリファクタリング

しばらく時間をおいてコードを見てみると、かなりひどいスパゲティコードでしたので、まずここのリファクタリングをしました。一月の私が何を考えていたのかまったくわかりません。

リファクタリングの形跡

なんでもかんでもバトルマネージャでやっていたので、バトルステージマネージャを作って初期設定をそっちにやらせたり、なぜか行動選択(攻撃/スキル/アイテム/逃げる)パネルが他パネルの表示制御とかまで全部やっていたのでこれも別クラスに移譲したり。今まだスキルパネルが選択されたスキルを判断して次のパネルを決定(回復なら味方パネル、攻撃なら敵パネル)したりしているんですが、これはちょっと保留。

テストは導入してみたもののちょっと何をどう書いていいのかさっぱりわからないので結局書いていません。ダメージ計算機とかのテストは書くと思います。

UI周辺のロジックの修正

Toggleから情報を受け取る都合上、スキルとかターゲットとか全部文字列で受け取ってしまっていて、じゃあこのスキルは単体攻撃なの?全体攻撃なの?って判断が苦しかったので、Toggle継承クラスを修正してオブジェクトを持つようにしました。

public void SetObject(Creature creature)
{
    value = creature.displayName;
    this.creature = creature;
    gameObject.GetComponentInChildren<TextMeshProUGUI>().text = value;
}
public void SetObject(SkillSetting skill)
{
    value = skill.skillName;
    this.skill = skill;
    gameObject.GetComponentInChildren<TextMeshProUGUI>().text = value;
}
// public void SetObject(Item item) {
//     value = item.itemName;
//     this.item = item;
// }

public T GetObject<T>() where T : class
{
    if (typeof(T) == typeof(Creature))
    {
        return creature as T;
    }
    if (typeof(T) == typeof(SkillSetting))
    {
        return skill as T;
    }
    return null;
}

これオーバーロードにする意味あったんだろうか?あとで修正するかもしれません

その他、ToggleGroupにも「全部を選択しいてる(風)状態」を作るためのコードを書きました

 public void selectAll()
    {
        foreach (ToggleInherit toggle in toggles)
        {
            if (toggle.transition == Selectable.Transition.SpriteSwap)
            {
                var image = toggle.targetGraphic as Image;
                if (image != null)
                {
                    image.sprite = toggle.spriteState.highlightedSprite;
                }
            }
        }
    }

かなりわかりづらいとは思いますが、すべてのトグルにカーソルが当た(っている風の状態にな)ります。

全体攻撃

というわけで、表面上の機能はそこまで変わっていませんが、進捗報告でした。
次はダメージ計算をしたいですね

Terrainに木を生やす

やっと生やせたので覚書。
参考にしてたサイトはたぶん全部アセットストアとかの素材を使っていて、そのせいで触れられていなかったのだと思う。LODGroupの設定が必要。

tree(Empty)の下にtreeとtree_mとtree_sを入れています。これが何かっていうと、たぶん遠くのオブジェクトを軽量化するための設定。遠いほど荒いみたいなやつ。
tree(Empty)にLODGroupコンポーネントを入れて、それぞれにそれぞれのモデルを設定して

tree(Empty)ごとPrefabにして、そのPrefabをTerrainのTreeに登録して、やっと木を生やせた。足かけ一年くらい悩んだんじゃないかこれ

なんか地面に埋まってる&マテリアルが当たっていないのでまだ調整

ツタ・ツル系の敵のモデリング

ツタ系の敵

ビターン!って感じで攻撃してきて毒とかマヒとかを付与してくるタイプの敵。森ステージに出てくる。
ドロップ品は棘で、矢尻として使用できるアイテムになる想定。 しかしUIとして「矢尻を選んで攻撃」ってどうしたもんかな

劇場アニメーション「犬王」(2022)

あらすじ(Huluから転記)

室町の京の都、猿楽の一座に生まれた異形の子、犬王。周囲に疎まれ、その顔は瓢箪の面で隠された。ある日犬王は、平家の呪いで盲目になった琵琶法師の少年・友魚と出会う。名よりも先に、歌と舞を交わす二人。友魚は琵琶の弦を弾き、犬王は足を踏み鳴らす。一瞬にして拡がる、二人だけの呼吸、二人だけの世界。「ここから始まるんだ俺たちは!」。壮絶な運命すら楽しみ、力強い舞で自らの人生を切り拓く犬王。呪いの真相を求め、琵琶を掻き鳴らし異界と共振する友魚。乱世を生き抜くためのバディとなった二人は、お互いの才能を開花させ、唯一無二のエンターテイナーとして人々を熱狂させていく。頂点を極めた二人を待ち受けるものとは――? 歴史に隠された実在の能楽師=ポップスター・犬王と友魚から生まれた、時を超えた友情の物語。

作品関連情報

www.hulu.jp

原作小説

www.kawade.co.jp

感想

前情報なしで見たら「なんか想像と違う話になってきたな?」っていう気持ちの連続だった。面白かった
終わってから改めてあらすじを読むと「まあ、そうだな」という気持ちになるんだけど、あらすじを先に読むと「え?」となる気がする。
なにしろこの映画の大半(たぶん。計ってはいない)をライブシーンが占めているので、「ストーリーのあらすじ」っていうと上記の通りなんだけど、受ける印象はかなり異なる。

途中の犬王と友魚のライブシーン、琵琶と太鼓なんだけど、どう考えてもアンプとエフェクターをかませたエレキギターの音がしてだいぶ面白い。そりゃあこんな時代にこんな音楽と舞いがあったらご禁制にもなりますわ。脳内麻薬じゃぶじゃぶ出るだろうな
ひとのレビューとかを読むと、だいたいこの辺りで賛否わかれるっぽい? 私は好きです。面くらいはしたけど。

13分頃の、石碑を触って文字を確かめるシーン。これゲームの演出にもいいなーと思った。盲目の主人公が聴覚や触覚で世界を広げていく演出、良い。映画でも印象的で良い表現だけど、インタラクティブな媒体にも合うと思う。
21分頃の演出も好き、友魚が聞いている音とか嗅いでいる匂いから周りの様子を知っているような、いい演出だと思う。

なんか冒頭から源平の話が出てくるので、もうちょっと重苦しい話だと思っていたのだけど、全体的に結構明るかったな。ラストだけ胸糞だったけど。っていうか冒頭(友魚が視力を失うあたり)とラスト(友魚が名を失うあたり)を総合して考えるとクッソ胸糞ではあって、でも全体的には明るくて、ちょっと総合的な評価をどこに置いていいかよくわからない。胸糞シーンと熱いシーンが交互に来る感じ。そりゃ評価割れるよなという気はする。ひとつの結末に向かって収束する感じが薄いというか。史実ありきだから仕方ないのかも。

ラストだけ見た母が「なんで顔隠してたの?」と仰るので冒頭見せたらショックだったのかしょぼしょぼになっておられた。確かに「なんか得体のしれないものが実は人間だった/呪いを跳ね返して成功していく」っていう順番で見るのと「人間がかつて得体のしれないものだった/犬ころのように扱われていた」という順番で見るのでは印象が違うだろうな。

丁度「逃げ上手の若君」を見ていたので、時代が近くてそれもちょっと面白かった。鎌倉時代南北朝時代なので、人間の尺度としてはともかく、歴史の一部としては結構時代が近い。
逃げ若冒頭(鎌倉幕府の滅亡)が1333年、犬王(後半)は足利義満が日野業子を正室に迎えた後っぽいので1374年ころ? 足利義光は足利尊氏の孫なので、そのあたりを考えると面白いし日本史マジでよくわからんな。軽くおさらいしてもいいかもしれん

ロスト・ケア(2023)

あらすじ(Huluから転記)

早朝の民家で老人と訪問介護センターの所長の死体が発見された。捜査線上に浮かんだのは、センターで働く斯波宗典。だが、彼は介護家族に慕われる献身的な介護士だった。検事の大友秀美は、斯波が勤めるその訪問介護センターが世話している老人の死亡率が異常に高く、彼が働き始めてからの自宅での死者が40人を超えることを突き止めた。真実を明らかにするため、斯波と対峙する大友。すると斯波は、自分がしたことは『殺人』ではなく、『救い』だと主張した。その告白に戸惑う大友。彼は何故多くの老人を殺めたのか? そして彼が言う『救い』の真意とは何なのか? 被害者の家族を調査するうちに、社会的なサポートでは賄いきれない、介護家族の厳しい現実を知る大友。そして彼女は、法の正義のもと斯波の信念と向き合っていく。

キャスト(敬称略)

長澤まさみ - 検事・大友秀美
松山ケンイチ - 訪問介護センター職員・斯波宗典

作品関連情報

Hulu
www.hulu.jp

原作小説


honto.jp

感想

検事の仕事がどこからどこまでなのか知らないので、大友秀美(長澤まさみ)がなぜ斯波宗典(松山ケンイチ)と話してるのかよくわからなかった。
容疑者からの聴取がまあ検察の仕事だとして、だんだんただ「言い負かしたい」だけで話しているように見えてきて、多分あれは実際に言い負かしたいだけなんだろうなと思う。
メタ的なことを言うなら単純に「検事は読者の側に立って物語を進めるストーリーテラーなのでこういう問答をしてます」で終わる話なので、以下メタ的なことは考えない考察。
大友秀美は自分の両親のことがあって、後ろめたくてしんどくて怖くてつらくて、その反対側に愛情とか正義感とかがあって、斯波宗典を責めたくて攻撃したくてたまらない。つまるところ映画のラストで「ひとごろし」と叫んだあの人と同じなのだと思う。ただそこに職業だの立場だのなんだの余計なものがくっついてややこしい出力になっているだけなんだろうな。あの激昂の中身ってたぶん別に正義感とかじゃないでしょ。おとなしい容疑者をあんなに怒鳴りつけたりして、実はコンプライアンス的にはアウトだったりしません?

斯波宗典も斯波宗典で多分実の親から「ころしてくれ」と頼まれたことがつらくて怖くて、でも自分ももう限界で、実の親を手にかけて本当にもう駄目になってしまって、すがる先が「救済の執行」しかなくなってしまったんじゃないか。介護センター職員として、あるいは救済の執行者として、両面から介護する側/される側を「救って」いる。実際に救済しているのは「実の親を手にかけてしまった過去の自分」だと思うけど。正当化しなきゃやってられないよな。
私は正直なところ斯波宗典のやったことには割と賛成の立場で、別に「ころしていい」と思っているわけではないけど、実の親を手にかけるのはつらいから、どこかの誰かが代わってくれるなら、それは実際に「救い」なんじゃないかしらとは思う。明確な八つ当たり先までできてたいへんにお得である。自分は善良な被害者の立場のまま、介護から解放されることができる。よかったね。
斯波宗典の父親が「お前を覚えているうちに死にたい」と泣いていたけど、子供の側からしても「あなたを好きでいられるうちに死んでくれ」と思うんじゃないか。介護に疲弊して、少なくとも介護する程度には愛情が残っていたはずの親を目の前に「いつになったら終わるんだ」と思う瞬間がどんなにつらいか、私は想像しかできないけど。
まあ同意書くらいは作っとくべきでしたねとも思う。嘱託殺人ならちょっと罪が軽いので(対象者に認知症とか入っちゃってると同意書も効力無さそうな気はする)

後半の春山登と羽村洋子のシーン、
春山「ぼくは洋子さんより歳が上だから、後々迷惑かけてしまうんじゃないかって」
羽村「それでも一緒にいたいと思ってくれたんですよね」
春山「それはもちろん」
っていうやりとりの意味がわからなかった。迷惑かけられる側(羽村)が「それでも一緒にいたい」って言うならわかるんだけど、それ春山の方から言うん? 逆じゃね?っていう
「迷惑かけるかもしれないけど一緒にいたい」って何? 接続詞合ってる?

裁判の後の、大友秀美が斯波宗典に自らの過去を話すシーン、なんでそれを「自分が担当した事件の容疑者」に話してるんです?カウンセリングに行きな?

戦闘画面のUIを作る

何回目だこの話題
いちおうスキル使用までは見たんですが、アイテム使用はどうなるかまだわからないので今後も修正あるかもしれません。

前回次点だとトグルにリスナーを付けて選択の結果をバトルマネージャで受け取ってたんですが、あれだとバトル進行的に都合が悪く、結局は一部awaitで選択を待つ形式になりました。

すべてのToggleGroupを包括するCanvasスクリプトを設置

我ながら流石にわかりづらかったので追記: panelsに敵選択パネルと味方選択パネルのみを指定します。 以前のコードではawaitですべてのパネルを数珠つなぎに実装していたんですが、cancellationTokenを呼んだときにすべてのパネルがキャンセルされてしまっていました。 今回のコードでは「攻撃対象(スキル使用対象)の決定」という一番最後の動作だけを待つ形にしています。

   public async UniTask<Action> selectAction(CancellationToken cancellationToken)
    {
        // selectActionPanel.SetActive(true);
        // await selectActionPanel.GetComponent<ToggleGroupInherit>().selectAsync(cancellationToken);
        await UniTask.WhenAny(panels
            .Select(panel => panel.selectAsync(cancellationToken)));
        string actionType = selectActionPanel.GetComponent<ToggleGroupInherit>().getValue();
        string skill = selectSkillPanel.GetComponent<ToggleGroupInherit>()?.getValue();
        string item = selectItemPanel.GetComponent<ToggleGroupInherit>()?.getValue();
        // string enemy = selectTargetEnemyPanel.GetComponent<ToggleGroupInherit>()?.getValue();
        string[] enemies = selectTargetEnemyPanel.GetComponent<ToggleGroupInherit>()?.getValues();
        foreach(string enemy in enemies){
            Debug.Log("test: "+enemy);
        }
        // string[] enemies = new string[] {enemy};
        string ally = selectTargetAllyPanel.GetComponent<ToggleGroupInherit>()?.getValue();
        string[] allies = new string[] {ally};
        Debug.Log("action: " + actionType);
        Debug.Log("skill: " + skill);
        Debug.Log("item: " + item);
        // Debug.Log("enemy: " + enemy);
        Debug.Log("ally: " + ally);

        return new Action(
            actionType,
            skill,
            item,
            enemies,
            allies
        );
    }

バトルマネージャではこれを待ち受け

   private async UniTaskVoid Battle()
    {
        cancellationToken = this.GetCancellationTokenOnDestroy();
        do
        {
            Character actioner = allies[0]; // TODO
            // IEnumerable<string> skillNameList = actioner.skills.Select(x => x.skillName);
            // selectSkillPanel.GetComponent<ToggleGroupInherit>().regenerateButtons(skillNameList);
            Action action = await selectActionPanel.selectAction(cancellationToken); // ←ここ
            // Debug.Log(actioner);
            actioner.execute(action);
        } while (BattleContinue);
        SceneManager.LoadScene("ResultScene");
    }

ここでキャラクターのアクションを起動

actioner.execute(action);

executeの中身はこう

   public async UniTaskVoid execute(Action action)
    {
        switch (action.actionType)
        {
            case Action.ActionType.Attack:
                anim?.SetBool("Attack", true);
                await UniTask.Delay(TimeSpan.FromSeconds(1f));
                anim?.SetBool("Attack", false);
                break;
            case Action.ActionType.Defense:
                anim?.SetBool("Defense", true);
                await UniTask.Delay(TimeSpan.FromSeconds(1f));
                anim?.SetBool("Defense", false);
                break;
            case Action.ActionType.Skill:
                Debug.Log("スキル");
                Debug.Log(action.skill);
                SkillSetting skill = skills.Where(skill => skill.skillName.Equals(action.skill)).First();
                anim?.SetBool(skill.skillName, true);
                await UniTask.Delay(TimeSpan.FromSeconds(1f));
                anim?.SetBool(skill.skillName, false);
                break;
            case Action.ActionType.Item:
                anim?.SetBool("Item", true);
                await UniTask.Delay(TimeSpan.FromSeconds(1f));
                anim?.SetBool("Item", false);
                break;
        }
    }

スキルとかのデータはプレハブに直接持たせている

これで画面からアクション選択→キャラクターのモーション発火までは動く。はず(Blender側の作業をしていないので確認もしていない)

目下の問題点 - 「いま行動するキャラクターが持っているスキルだけを表示する」みたいな機能はないのですぐぬるぽで落ちる - 全体攻撃とかの処理を考えていない

コードはこのへん github.com