<前文>
Genocideそして「進撃の巨人 Final Season」Part 2
先週、GenocideとMassacreは日本語に訳すとどちらも虐殺になりますが、英語でGenocideはある民族を完全に消滅する事で単なる虐殺であるMassacreとは全く違う日本には存在しない概念であり、更に世界ではGenocideは身近であると話しました。
そして「進撃の巨人 Final Season」の隠れたテーマがGenocideであるが故に日本ではあまりヒットしないにも関わらず日本以外の世界中で大ヒットしているとの話を今週はします。
ここからは「進撃の巨人 Final Season」のGenocideを軸としての私の個人的な解説をしますが、詳細は間違っているかもしれません。忙しいのであんまりアニメを見る時間がないのでその辺はご了承下さい。
「進撃の巨人 Final Season」では世界中の民族が主人公の民族をGenocideしようとしています。所が数年の間だけですが、主人公の民族は強力な兵器を所持していてそれを使用すれば逆に世界の主人公以外の民族を滅ぼす事が出来ます。
ここで3つの選択が出来ます。
- 今すぐ戦争を仕掛けて、世界を滅ばして自分たちだけ助かる
- 世界と話し合って、お互いにGenocideしないよう約束して共存共栄する。
- 自分たちだけ助かるために自分たち以外の全ての民族を滅ぼすのは罪が重すぎるので、自分たちが絶滅するのを受け入れる。
1の選択は最も現実的ですが、最もモラルに反しています。簡便に検証しても以下の道徳に反しています。
- 自分たちの民族がGenocideされるのを避けるために、他の民族はGenocideしても構わないと言う矛盾。
- 更に現状では、奴隷みたいな立場ですが、一応服従を誓えば、主人公の民族もギリギリGenocideされないで生きていける選択もあります。それを捨てて自分たちの民族がGenocideされる前に自分たち以外の全ての民族をGenocideしてしまおうという自分勝手な選択。
2の選択は最も理想的ですが、現実に達成可能なのか?常に疑問が付きまといます。例えば、
- 数年経った後で、世界の軍事力が主人公達の民族より強大になってから約束を反故にされる可能性。
- 主人公達の民族が持つ強大な軍事力を放棄する事を条件に、お互いにGenocideしないよう約束するが、放棄した途端に攻めて来られる可能性。
3の選択は人間としての道義を守り、かつ実現可能でもありますが、主人公達の民族にとっては悲惨過ぎる結末しか待っていません。
平均以上の常識と知性がある人なら、全ての人にとって受け入れられる選択は2しかないと直ぐに分かります。そして2を選択しつつ最悪3になったらそれも受け入れるしかない位の結論に到達するはずです。
所が、このアニメの主人公、何のためらいもなく1を選択します。
この主人公は世界を弱肉強食、勝者のみが自由を手に入れる事が出来る残酷なものと見なしているからです。主人公にとってはGenocideするかされるか以外の選択は存在しないんです。そしてその戦いに勝ったものだけが自由を手に入れられると心の底から思っています。
この主人公の選択を骨董無形で自分勝手と断ずるのは簡単ですが、世界の視聴者はこの主人公の決断を自分たちの環境に置き換えるとGenocideが身近に在るが故に真剣に受け止めざる得なくなるのです。
その結果「進撃の巨人 Final Season」は世界で大ヒットをし続けるのです。
それでは今週の勉強を始めます。
<本文>
1.今週の予定
今週はセーブ機能を完成させます。
- Game Instance BPクラス内のセーブに必要な変数の値をsave出来る様にする。
- UE4C++でのGame Instanceクラス内の変数の値をsave出来る様にする。
- 神官のSave機能を実装する
これらをやって行きます。
2.Game Instance BPクラス内のセーブに必要な変数の値をsave出来る様にする。
2.1 先週の調査の復習
先週の調査でRPGGameInstanceBP内の変数でSave/Loadする時に必要な変数は以下の3つでした。
ただしMonster Spawn Dataはやっぱり無くても良い事になったので結局はMap変数とItem Spawn Data 変数の値だけです。
これらの変数の値がセーブ出来るようにします。
2.2 Map変数の値がセーブされるようにする
Map変数は以下に示した様にEnumクラスから作成されています。
まずこの変数をSave Gameクラスから派生して作成したクラスに追加してみます。
あれ、SaveGameクラスから派生したBPクラスが見つかりません。
うーん。
調べたら前回は以下の方法でSave機能を作成していました。
ポーズ画面でセーブボタンを押すと、
GameInstanceの関数、SaveMyGameを呼び出します。
SaveMyGameはUE4C++のRPGGameInstanceで作成されています。
実装部を見ると、SaveGameクラスから作成したMySaveGameクラスが使用されています。
MySaveGameクラスは以下のように作成しています。
うーん。このやり方だとBPにある変数はSave出来ないですね。
この関数は取りあえず維持したまま、MySaveGameクラスからBPを作成してそこに変数を追加していきます。
Enum、MapクラスからMap変数を作成します。
Enumの名前をMapにしてしまったために、Map(dictionary)と紛らわしくなってしまいました。LevelNameとかにすれば良かったです。
後、Pauseメニューからはsaveが出来る仕様にはしない事にしたので、この機能は後で消します。代わりに魔法ボタンを追加します。
先週作成した、神官の会話UIのセーブボタンに
以下の実装を追加します。これでSlotにsaveされるはずです。
Slot名は前に作成したSave機能と間違えないために新しい名前のMySavedGameにします。
Load機能は以下のスタート画面のロードするボタンをクリックした時に実行するようにします。
現状は以下の様になっています。
以下の様に変更しました。
何で、GameGold変数の値がBPから変更出来るのか?
UPROPERTYでBlueprintReadWriteに指定されていました。
後、EditAnywhereに指定されていますが、このコードを書いたとき、今一EditAnywhereの意味が分かってなかったのでしょうね。
以下に示した公式のこのサイトに
EditAnywhere、EditDefaultOnly、そしてEditInstanceOnlyの違いが説明されていますが、
ここで大切なのは、ArchTypesとInstanceの違いです。ArchTypesとは簡単に言えばクラスの事で、EditDefaultOnlyにした場合、その変数はそのクラスで一個の値しか持てません。EditInstanceOnlyはその逆で、その変数の値はそれぞれのInsntanceによってバラバラの値になります。EditAnywhereは、ClassからでもそれぞれのInstanceでも値の指定が出来ます。
と言う事です。
Game Instanceは一個しか作成されないのにそのクラスの変数がEditAnywhereを持つ必要はないはずです。EditDefaultOnlyに変更しました。
テストします。
Saveは普通に出来たみたいです。
Loadします。
エラーになってゲームが強制終了になってしまいました。
デバックして調べて見ると、Loadの実装の最後のOpenLevelでエラーになっていました。
何で?とLevelNameにパスされている値を見たら、以下の様に成っていました。
理屈は良く分からないのですが。LoadしたGameInstanceの変数から参照したらmap1と表示されたのでこっちを使用します。
テストします。
またエラーになってしまいました。
調べたらMapからStringに変換した時はmap1が表示されますが、MapからNameに変換した時は、Map::NewSameEnumerator1が表示されました。
じゃ、一端Stringに変換してNameに直してみます。
テストします。
今度は出来ました。
所持金も金貨95枚になっているので、Saveした時に神官に支払った金貨5枚が減った状態できちんとセーブされていました。
やっと出来ました。
2.3 Item Spawn Data 変数の値がセーブされるようにする
ItemSpawnDataはクラスItemSpawnDataのarrayですね。
まずItemSpawnDataクラスを調べます。
調べたらItemSpawnDataはStructでした。
これはShallow copyになりそうですね。
取りあえず、MySaveGameBPにItemSpawnDataクラスのarrayを作成します。
ここにRPGGameInstanceBPの値をセットします。
ここまでやってあれなんですがArrayの前にStructのそれぞれの値がSaveGameクラスでどのように保持されるのか知りませんでした。こっちから調べます。
2.3.1 StructのデータとSaveGameクラス
StructのデータがどのようにSaveGameクラスに保持されるのかを調べます。
先程作成した、ItemSpawnDataのarrayを変数にします。
以下の方法でRPGGameInstanceBPの最初の要素の値をMySaveGameBPのItemSpawnDataにセットしました。
勿論、Get はCopyを使用しています。
以下の方法で、Slotから読み込んだMySaveGameBPのItemSpawnDataの値を見てみます。
普通に全部正しい値でセーブされていました。
おお。
これGetをreferenceにした場合はどうなんでしょうか?
GetがReferenceでも出来ていますね。
ここまでやって気が付いたんですが、Saveに必要な情報ってItemをSpawnするかどうかだけでした。Boolean変数のArrayを作成すれば良いだけでした。
もう一回やり直します。
MySaveGameBPの変数をItemSpawnBoolとしてBoolタイプのArrayにします。
以下の方法でRPGGameInstanceBPのItemSpawnDataの要素、Spawnの値をMySaveGameBPのItemSpawnBoolに渡します。
前にやったのと同じ方法で、Slotセーブしたデータから作成したMySaveGameBPのItemSpawnBoolの値を調べます。
MySaveGameBPのItemSpawnBoolの値はslotに正しくセーブされていました。
もう一度確認します。
こんどはアイテムを一個だけ回収した後で、セーブしました。
出来ていますね。
ではLoad時にこのArrayの値を元にRPGGameInstanceBPのItemSpawnDataの要素、Spawnの値を変更するようにします。
スパゲッティコードになりそうなので関数で実装しました。
中身は以下の様に成っています。
Arrayの関数をしっかり勉強したら、この方法より簡単な実装方法がありそうなんですが、とりあえずこれで行きます。一応これでもArrayの要素の一部の値の変更が可能なはずです。
テストします。
マップ内に配置されているアイテムの内、盾だけを回収しました。
Saveしてloadします。
盾だけ無くなっていました。
出来ました。
3.UE4C++のRPGGameInstanceクラス内の変数の値をsave出来る様にする。
UE4C++のRPGGameInstanceクラス内の変数でsaveする必要がある変数をSave/Load出来るようにします。
以下の変数がSaveする必要がある変数です。
上に行くほど、Save/Loadの作成が難しくなっているので下から作成していきます。
3.1 IsArmorEquippedの値をSave/Load出来る様にする。
この変数は、BlueprintReadWriteで指定されているし、単なるBoolean変数なので今までの知識で簡単にSave/Load出来るはずです。
MySaveGameBPに新しい変数、IsArmorEquippedを作成します。
最初はBPのMySaveGameBPでなくUE4C++のMySaveGameに作成した方が良いかと思いましたが特にUE4C++側で作成しなければならないメリットはないのでBP側で作成します。
以下の方法で実装しました。特に新しい事はしていません。
ここでテストしようと思ったのですが、以下に示した通り、RPGGameInstanceのArmorEquipped変数の値もセットされていないとエラーになってしまうのでこれをセットしてからテストします。
3.2 ArmorEquippedの値をSave/Load出来る様にする。
そういうわけでArmorEquippedの値を次にやります。
BlueprintReadWriteですし、やった事ないのはFStringだけです。
以下に示した様に、今までと全く同じやり方でやりました。
これでテスト出来るはずです。
木の盾を装備した状態でSaveしました。
Loadします。
以下に示した様に木の盾を装備して登場しました。
出来ました。
3.3 IsWeaponEquippedとWeaponEquippedをSave/Load出来るようにする。
3.1と3.2でした事と全く同じ事をIsWeaponEquippedとWeaponEquippedにします。
テストします。
剣を装備した状態でSaveし、その後、Loadしてゲームを開始しました。
剣を装備した状態から始まりました。
出来ています。
3.4 ItemsをSave/Load出来るようにする。
WeaponよりもItemの方が値段が安いのでテストしやすいと思い、Itemsを先にやる事にしました。
まずBlueprintReadOnlyで指定していますが、Arrayなんです。Loadする時Add関数を使用すればBPからでもこの変数に要素の追加が出来たはずです。となると敢えてBlueprintReadWriteに直す必要もない事になります。
UE4C++のRPGGameInstanceはそのままでやって行きます。
以下の方法でMySaveGameBPのItems変数にRPGGameInstanceのItem変数の値を移します。
Load時には、以下の方法でMySaveGameBPの値をRPGGameInstanceに移します。
見やすいように関数にしました。
テストします。
回復薬、イーサー、ダークイーサーを2個ずつ買いSaveします。
一端ゲームを終了してloadします。
道具袋を開いて見ると
買ったitemはきちんとSaveされていました。
出来てますね。
3.5 WeaponsをSave/Load出来るようにする。
Itemsと同じやり方でやります。
テストします。
武器屋は隣町にしか作っていませんでした。ので配置している武器を拾ってセーブしました。
Loadして装備品を開くと所持している武器が表示されていました。
出来ました。
3.6 MyYourHeroをSave/Load出来るようにする。
最も大変な変数の番が来ました。
GameCharacterクラスである MyYourHero変数をSave/Load出来る様にします。
まずこの変数はObjectなので単純にSave/Loadしたらshallow copyに成ってしまうのかどうかが分かりません。
次に、BlueprintReadOnlyに指定しているので、BP側から書き込みが出来ません。
この辺の問題を解決しつつMyYourHero変数をSave/Load出来る様にします。
3.6.1 MyYourHero変数とBlueprintReadOnlyについての調査
今、考えてみると、Levelが上がった時や、戦闘でダメ―ジを貰った時にMyYourHero変数の中の変数であるLevelやHPの値はBPで変更しているはずです。どうやっているのか調べて見ます。
因みにMyYourHero変数のタイプであるGameCharacterクラスを見ると
となっていて、BP側から値を変更出来るのかもしれません。
調べて見たら、全部UE4C++側で操作していました。
折角ここまでMyYourHero変数はUE4C++側で操作していたので、LoadもUE4C++側でやりたいですね。その方向でがんばってみます。
3.6.2 MyYourHero変数のSave
GameCharacterクラスをMySaveGameBP内に作成してSave出来るのかをテストする所から始めます。
以下の方法でRPGGameInstanceのMyYourHero変数をMySaveGameBPのMyYourHero変数にセットします。
Load時にMySaveGameBPのMyYourHeroオブジェクトがその変数であるPlayer Nameの値を保持しているのか以下の方法で確認します。
テストします。
思いっきりエラーになりました。
やっぱりSaveGameクラスは Objectの値は保持してくれないみたいですね。
もしくは、普通のC++のようにGameCharacterクラスにSet()関数をOverrideして書く必要があるのかもしれません。
それともArrayでやったように一個、一個の変数に値をSetすれば出来るのかもしれません。
簡単に出来る方法から試してみます。
3.6.3 MyYourHero Objectの変数の値を一個ずつsetする。
以下の方法で試してみます。
駄目でした。
3.6.4 MyYourHero Objectの全ての変数をMySaveGameBP内に作成する
このやり方なら絶対Save出来るのは分かっています。
一応試してみます。
テストします。
出来ています。
このやり方でsaveするとGameCharacterの内、以下の変数をコピーする必要があります。
ぱっと見た限りでは全部出来そうです。
このやり方でやってみます。
よく考えたらPlayerNameはセーブする必要なかったです。Occupationに変更してしまいます。
MHPをsaveします。
この変数、UE4C++側はInt32をタイプに使用しているのですが、BPにはIntegerとInteger64しかありません。Integerをそのまま使用してみます。
これでテストします。
出来ていました。
残りのMMP、ATK、DEF、LUCK、XP、LV、そしてMagicsを作成します。
テストします。
XPとLVは0と1なので出来ています。
こんどは魔法を覚えた状態でSaveしました。
魔法もsaveされています。
3.6.5 MyYourHero Objectの変数の値をLoadする。
RPGGameInstanceのMyYourHero のBlueprintReadOnlyの設定は今更変えたくないので、RPGGameInstanceにあるLoad関数を改良して値をパスしようと思ったのですが、MyYourHero変数のクラスであるGameCharacterのそれぞれの変数が、BlueprintReadWriteに設定されています。
それなのにRPGGameInstanceのMyYourHeroがBlueprintReadOnlyである意味はないと思われるので、RPGGameInstanceのMyYourHero の設定をBlueprintReadWriteに変更します。
そしてBP内でLoad時のRPGGameInstanceのMyYourHeroの値を変更します。
以下の状態でsaveします。
やっている事は今までと全く同じです。
テストします。
結果です。
Loadする時にHPとMPの値をMHPとMMPと同じ値にする必要がありました。
直します。
もう一度テストします。
出来ました。
正し、2つバグが出ました。
一つ目のバグはトラップに侵入すると発生するモンスターを倒してみたら出ました。
二つ目のバグはLevelが上がって魔法を覚えたら同じ魔法を二つ覚えていました。
これらのバグを直します。
4.神官のSave機能を実装する
この機能は既に3で作成したのでスキップします。
5.バグの直し
「3.6.5 MyYourHero Objectの変数の値をLoadする。」で発生したバグを直します。
5.1 トラップに侵入すると発生するモンスターを倒すと発生するエラー
これはもうDestroyActorが単純に要らないです。
と思ったら戦闘しないで逃げた時には必要でした。
以下の様に直しました。
テストします。
モンスターの縄張りから出ます。
一瞬でモンスターが消えました。これは素晴らしいですが、アニメーションがあった方が更に素晴らしいですね。
今度は戦闘をしてみます。
勝ちました。
元のマップに戻って来てもエラーは出ていません。正し、魔法を使用した後のモーションが素手の攻撃になっていました。前にこのバグは直したはずですが?
このバグも直します。
5.2 同じ魔法を二つ覚えるバグ
これは魔法を追加する時にAddUniqueを使用すれば直るはずです。
直しました。
テストします。
直っていました。
5.3 魔法を使用した後のモーションが素手の攻撃になるバグ
オカシイですね。このバグは前に直しておいたはずですが。
もう一度確認します。
直っていません。
Blogを確認したのですが、どこでこのバグを直したのか分かりません。
取りあえず以下の方法で直しました。
戦闘中のアニメーションをplay animationノードで実装しているため、animationが終わった後でMy Third Person_AnimBPをセットし直す必要があります。
この時にMy Third Person_AnimBPの設定が初期化されてしまうため、もう一度、My Third Person_AnimBPの変数with Weaponを設定し直す必要があります。
これがバグの原因でした。
このPlayAnimationを使用するのは、あまりよろしくないです。全てのアニメーションはMy Third Person_AnimBPで管理すべきです。これも後で直します。
5.4 ポーズ画面のセーブボタン
ポーズ画面にまだセーブボタンが残っていました。
魔法でも追加しようと思いましたが、更に複雑にしても管理出来ないのでオプションにします。
オプションの内容は後で考えます。
5.5 罠モンスターが消える時のアニメーション
一瞬で消えるのは変なのでアニメーションを追加しました。
本当はモンスターが発生するアニメーションを逆再生したのを追加したかったのですが、PlayAnimationにその機能がないのでモンスターが倒された時のアニメーションを追加しました。
テストします。
縄張りから出ると見事に倒れました。
出来てます。
6.Save/LoadのBPの実装部の整理
Save/Loadの実装をBPで作成しましたが、結構ぐちゃぐちゃしています。整理します。更にRPGGameInstanceクラスのSave関数のような必要のない関数や変数を消します。
6.1 BP内のSave/Loadの実装を整理します
どちらが見やすいでしょうか?
見た目の綺麗さは同じ位だが、Reroute node を使用している方が同じクラスの変数に値をセットしている事が理解しやすい。
Reroute nodeを使用した形に書き直してみます。
この部分だと見やすさはそんなに変わらないですが、
この辺は段違いに見やすいです。
正し、Arrayの要素に値をパスする所は見にくくなっています。関数を作成して見やすくします。
関数にしたら見た目はすっきりしたのですが、Errorが出てしまいました。
だそうです。
もうこの二つのarrayをBlueprintReadOnlyにして置く理由が全くないのでBlueprintReadWriteに変更します。
はい。直りました。
次はMyYourHeroオブジェクトのそれぞれの変数の値をSaveGameクラスの変数にセットしている所ですが、コレも一つの関数にします。
しました。
大分すっきりしました。
このノードらのしている事はGameInstanceの値をSaveGameに移しているだけなので一個の関数にまとめます。
実際のsaveのノードは以下の3つになりました。
非常に見やすいです。コメントも付けておきます。
Loadの実装も同様に整理します。
関数Load Gameに全てまとめました。
Load Gameの中身は、
となっています。SetMyYourHeroノードの中身は
となっています。
綺麗且つ読みやすいような整理は出来ましたが、ひょっとすると何処かに手違いがあって正しくsave/load出来ないかもしれません。確認します。
この状態でSaveします。ここには表示されませんが、魔法は炎(小)、炎(大)が使用出来ます。
Loadします。
HP102、MP22、経験値100、レベル3とセーブした時と同じになっています。所持している金貨は70枚から65枚に減っていますが、セーブした時に神官に金貨5枚払っているのでこれで正しいです。
AP52、DP32、LK2全て合っています。所持している道具は、回復薬、イーサー、ダークイーサーがそれぞれ一つずつでこれも合っています。
装備されている武器、盾は短剣(小)と木の盾(小)で合っています。
戦闘をして使用出来る魔法の確認をします。
はい。出来ています。
6.2 RPGGameInstanceクラスのSave/Load関数を消去します。
以下の関数を消します。
ただ完全に消去してしまうと、後でUE4C++側でsave/loadしたい時にどうやるのか分からなくなってしまうので、Comment outするだけにします。
Buildします。
出来ました。
これらの関数を使用してる箇所はないはずなのでこれで大丈夫なはずです。
7.ObjectはSaveGameクラスに保持させられないのかの検証
まず全てBPで実験してみます。
新たにProjectを作成するのも勿体ないので、先々週、AIの勉強に使用したProjectで検証します。
以下の方法で試します。
まずActorクラスからTestActorを作成し変数HPを追加します。
このTestActorのinstanceを一個Level上に配置します。
配置したTestActorのHPの値がI をクリックするたびに1だけ減るようにセットします。
Save/LoadそしてHPの値の表示様にWidgetを一個作成します。
配置しているTestActorをParameterとしてパスする設定にします。
HP、Saveボタン、Loadボタンを作成します。
HPの値、Saveボタン、Loadボタンの機能を実装します。
ここでSave/LoadするのはTestActorのInstanceそのものです。
このやり方でHPの値がSave/Load出来るのなら、少なくともBP内で完結すればObjectのSave/Loadは出来ると言う事になります。
Widgetを表示させます。
GameModeBaseBPクラスからWidgetを作成しようと思ったのですが、これだけのために新しいGameModeBaseBPクラスを作成するのも面倒なのでLevelBP内に作成しました。
これでテストします。
配置しているTestActorのHPを100から91に下げました。
Saveします。
SaveしたTestActorのHPの値が91である事は画面左上にプリントされた値からも確認出来ます。
一端ゲームを終了してLoadします。
Loadしましたが、前のHPの値はSaveされていませんでした。
出来ませんでした。
つまりSave/Load出来るのはその変数が保持している値のみのようです。その変数が保持している値が、アドレスの場合は、そのアドレスの値をセーブすると思われます。
となるとUE4C++でcopy constructorをoverrideして作成すればObjectでもSave/Load出来るのでしょうか?
そんな気はしますが、それを今試す必要はあまり感じないので、今回のSaveGameの検証はここまでで終了にします。
今回の検証だけではSaveGameクラスでObjectはsave/load出来ないと断言は出来ませんが、SaveGameクラスを使用する時、その変数の値が正しくSave/Loadされているかの確認は必ずすべきである。位は言えると思います。
8.まとめと感想
今週はこれで終わりです。一応Save機能も完成しました。来週は戦闘中に使用しているPlayAnimationの箇所でも直そうと思います。