<前文>
最近、firebaseの勉強を兼ねてウェブプログラミングの復習をしているのですが、ネット上におけるソフトウェアの真の価値に気が付きました。人がネットをやる理由は一つです。欲しい情報を得る。で、そのためにする事がGoogleで検索です。ここでみんな勘違いしているのが、Googleが欲しい情報をくれるわけではない事です。Googleは貴方がほしい情報がありそうなサイトの情報を教えてくれるんです。つまり答えを教えるのではなく答えがある可能性が高い場所を教えているんです。これがネットにおけるソフトウェアの真の価値だったのです。つまりネット上におけるソフトウェアはゴールドラッシュにおける金ではなく、金を掘る道具や金脈を見つけるための地図だったんです。そして悪魔でも金を見つけるのはユーザーだったんです。
そして思ったのですがゲームも同じだったんです。ユーザーがそのゲームをプレイする事で金を発見出来るのか?それこそがそのゲームの真の価値だったんです。
<本文>
今回は、前回勉強した内容の考察を行います。
<考察>
前回、何をやったのかをもう忘れてしまったのでもう一度前回のブログを読み直します。ブログを書くのはメンドクサイですが復習する時は本当に役に立ちます。
<Step.1>
- 意思の決定(decision maker)をどのタイミングでプレイヤーに任せるのかを変更します。
- プレイヤーが作成された時に意思の決定(decision maker)を任せるのではなく、戦闘が開始した時にプレイヤーに意思の決定(decision maker)を任せ、戦闘が終わった時に意思の決定(decision maker)を解消します。CombatUIWidgetクラスに意思の決定を実装する事で可能になります。
とありました。そうか、意思の決定(decision maker)の実装をCombatUIWidgetクラスに変更したんですね。ウーン。正直何で戦闘が開始した瞬間に意思の決定をプレイヤーに任せるべきなのか、最初にプレイヤーが作成された時ではいけないのかが分かりません。まず意思の決定(decision maker)が現状、
戦闘時におけるプレイヤーの選択が終了したかどうかを表しているだけなんです。まあ、戦闘中にしか使用しない関数ならびに変数ならば、戦闘開始時に初期化して戦闘終了後に開放すべきですね。それだけの理由かもしれませんね。
実際にStep.1で行ったのは、
GameCharacter.cpp のCreateCharacterから以下のコードの行を削除します。
character->decisionMaker = new TestDecisionMaker();
だけです。
これだと、decisionMaker()関数自体はGameCaracterクラスに残っています。前述の理由で意思の決定(decision maker)の実装をCombatUIWidgetクラスに変更したのなら、GameCaracterクラスのdecisionMaker()関数はもういらないと言う事になりますが、教科書ではしっかり残しています。これから変更されるのでしょうか?それとも全く別の理由で、意思の決定(decision maker)の実装をCombatUIWidgetクラスに変更したんでしょうか?
この回答は私がもっとRPGゲームにおけるソフトウェアデザインの全容が理解出来ないと分からないような気がします。何カ月か後で、もっと理解が進んでから考える事にします。
<Step.2>
次にGameCharacter.cpp のBeginDestory関数内で、deleteのラインをif節でラップします。
む。先週、こんな事していたのか?if節で囲ったと言う事は、decisionMaker変数自体が初期化される場合もあるのでしょうか?
次が問題です。
これの理由は、プレイヤーのための意思の決定(decision maker)はUIになるが、そのUIを手動で開放したくない(するとUnrealがクラッシュするから)からです。
その代わりに、UPROPERTYで修飾されたポインターがそれを指していない限りUIは自動でガーベージに集められます。
UPROPERTYで修飾されたポインターがそれを指していない限りUIは自動でガーベージに集められます。
この文章で先週は完全に理解出来なくなってしまったのでした。先週の疑問をそのまま以下に示します。
やっぱり時間を置いてもう一度考えるのは大切ですね。教科書はUUserWidgetクラスから派生したクラスから作成されたUIについて語っています。なのに、先週の私は一般的なUE4C++の変数についての解説と勘違いして読んでいました。教科書の説明がUserWidgetについてなら矛盾はありませんね。UUserWidgetクラスはそういうものと教科書は言っていて、私はUUserWidgetについて何にも知らないのでそうですか分かりました。で終わってしまう話です。
先週の疑問の一つがもう解決しました。時間をかけてじっくりやる事の大切さが実感できます。
<Step.3>
次にCombatUIWidget.h内でIDecisionMakerのインターフェイスを実装するクラスを作成します。
そしてBeginMakeDecision()とMakeDesicion()をパブリック関数にします。
CombatUIWidget内から意思の決定(decision maker)の実装を行うので当然ですね。
先週のブログをみるとCombatUIWidget.hに以下のように宣言されています。
ウーン。教科書通りに書いたのですが、IDecisionMakerのインターフェイスを継承しているので、virtual とoverrideは付けるべきではないのかなと思います。
実際、TestDecisionMakerクラスでは、
virtual とoverrideがついていますね。
しかし先週はよく気が付きませんでしたが、このUIwidgetクラスがC++とブループリントさらにはUIを通してプレイヤーとを繋げる橋になっているんですね。感慨深いです。
Step.3では更に、CombatUIWidgetクラス内に、いくつかの関数や変数を追加しています。これらの関数や変数は、先週、既に解説しましたので今回はスキップします。
<Step.4>
先週、BeginMakeDecision()関数の実装で、ShowActionPanel()関数からブループリントへ移行するのでしょうか?と質問を残してます。
これから調べます。
しっかりShowActionPanel()関数はイベントとしてブループリント内で実装されています。
UFUNCTIONでBluepritnImplementableEventと指定するとブループリント内でイベントとして呼び出せるみたいですね。
次は、AttackTarget()関数の実装についてです。
先週の疑問もそのままここにコピーしておきます。
- AttackTarget()関数を実装します。currentTarget変数のcombatAction変数をこの関数内で初期化したAction変数で指定します。
- うーん。これって勝手に開放されるのでしょうか?まずcurrentTarget変数自体がUPROPERTY()で指定されていません。UPROPERTY()で指定されていない変数が自然に開放されるのかどうかがまず分かりません。そこは置いておいても普通のC++で考えてみればnewで指定したメモリー領域は勝手に開放されないからどこかで開放しないといけないと思います。
- うん。良く分かりませんね。ここも考察で考えてみます。
これもUUserWidgetクラス内の話なので勝手に開放されるんじゃないでしょうか?特に深く考える必要はないと思います。
ここまで来て、重大なミスに気が付いたのですが、教科書の言う、意思の決定(decision maker)とはGameCharacterクラスにあるメンバー変数、
の事でした。このメンバー変数にいつ値を割り当てるのかについて述べていたのでした。何か勘違いしてMakeDecision()関数を言っていると思っていました。読み直してみると、この二つをごっちゃにして考察していますね。
これはやり直さないといけないですね。
もう一度、Step.1 からやり直します。
<Step.1(やり直し)>
まず、教科書の訳から間違えています。それから直していきましょう。
教科書の上記の文章を意訳したのですが、decision makerがGameCharacterクラスにあるメンバー変数を指していると判明した今では、訳もかなり変わってきます。まずthe playerでと言っていますがこのplayerはGameCharacterクラスから作成されたオブジェクトです。そしてdicision maker変数を当然持っています。ので、GameCharacterクラスから作成されたオブジェクト、playerのdecision maker変数がassignされます。と言う意味になります。
あ、そうだ。このassignedについても調べておく必要がありました。この場合のassignedは変数に単に値を割り当てる。つまりある変数を初期化するのではなく、単に前に初期化された変数のアドレス、もしくは値そのものを渡す事です。これの日本語訳が分からなかったです。
https://ejje.weblio.jp/content/assignによると
となっていました。これしか例が見つからなかったのですが、割り当てると訳がついていますが、例文では入れる。となっています。この訳では、例文のように明らかに変数に値を割り当てる場合以外は、初期化して値を割り当てているのか他の変数のアドレスを割り当てているのか分からないですね。アサインされる(assigned)と訳する事にします。
こういう細部に対するこだわりが足りませんでしたね。
- そのプレイヤーオブジェクト内のdecision maker変数がどのようにアサインされる(assigned)かを変えると言うアイデアがあります。
- そのプレイヤーオブジェクトが最初に作成された時にdecision maker変数をアサイン(assign)するのではなく、戦闘が開始した時丁度にdecision maker変数をアサイン(assign)します。
- そして戦闘が終了した時にそのポインターを開放します。
- そういう事が出来るCombatUIWidgetクラスを作成します。
と言う事が教科書は言いたかったのですね。
そしてStep.1で行った事はただ一つです。
GameCharacter.cpp のCreateCharacterから以下のコードの行を削除します。
character->decisionMaker = new TestDecisionMaker();
プレイヤーオブジェクトが最初に作成された時にdecision maker変数をアサイン(assign)するためのコードを消しました。
<Step.2(やり直し)>
特に変更は要らないですね。Step.2では、DecisionMaker変数しか出てこないので、MakeDecision()関数とごっちゃにして間違える箇所が、たまたま無かったからです。
<Step.3(やり直し)>
Step.3も変更は要らないですね。今度はMakeDecision()関数しか出てこないので、DecisionMaker変数とごっちゃにして間違える箇所が無かったからです、
<Step.4(やり直し)>
Step.4も変更は要らないですね。念のための先週のブログも読み直してみたのですが、先週のブログでは、DecisionMaker変数とMakeDecision()関数とをそれなりにきちんと分けて説明していました。ウーン。
<Step.5>
以下の事をStep.5では行いました。
プレイヤーキャラクターの意思の決定(decision maker)変数にIDecisionMakerのインターフェイスを実装したUIを割り当てます。
最初にRPGGAMEMode.cppのTestCombat()関数内で、キャラクターを反復(iterate)するループを変えます。
そうする事でUIを意思の決定(decision maker)として割り当てる事が出来ます。
これをみると、TestCombat()関数内でプレイヤーオブジェクトのdicisionMaker変数にUCombatUIWidgetクラスから作成したCombatUIInstanceオブジェクトをアサイン(assign)してます。これで、Step.1(やり直し)の
そのプレイヤーオブジェクトが最初に作成された時にdecision maker変数をアサイン(assign)するのではなく、decision maker変数の実装と戦闘が開始した時丁度にそれをアサイン(assign)します。
が達成されましたね。
先週、分からなかった全体の流れが見えて来ましたね。
<Step.6>
そして戦闘が終わった時にプレイヤーの意思の決定(decision makers)をnullにセットします。
これで、Playerオブジェクト内のdecisionMaker変数のポインターはGameModeクラスのCombatUIInstance変数のメモリーをシェアしなくなります。ので、
UPROPERTYで修飾されているCombatUIInstance変数はそのスコープが外れた時に自然に開放されるはずです。CombatUIInstance変数は、UCombatUIWidgetクラスから作成されているので、CombatUIInstance内の変数も自然と開放されるはずです。
<Step.7>
今やプレイヤーのキャラクターはUIを使用して意思の決定が出来る様になりました。
しかしながらUIは現状では何もしません。
機能を追加するためにブループリント内で作業をする必要があります。
とあります。つまりStep.1からStep.6までは、Step.1(やり直し)で述べた
- そのプレイヤーオブジェクトが最初に作成された時にdecision maker変数をアサイン(assign)するのではなく、戦闘が開始した時丁度にdecision maker変数をアサイン(assign)します。
- そして戦闘が終了した時にそのポインターを開放します。
- そういう事が出来るCombatUIWidgetクラスを作成します。
を行っていましたが、これからはCombatUIWidgetクラスのUIをブループリント側から作成すると言う事です。
ここから結構大変だった記憶があるのですが、このようなプログラミングをデザイナーだけに任せるのでしょうか?何となくC++はプログラミング、ブループリントはデザイナーのイメージがありますが単純にそうは分けれないと思いますね。この問題については最後にもう一度考えてみたいと思います。
Step.7では以下の事を行いました。
最初に、攻撃用ターゲットのオプション用のウィジェットを作成します。
- AttackTargetOptionと名付けます。
- ボタンを追加します。
- そのボタンにテキストブロックを追加します。
- Size to Contentにチェックを入れます。
- そうする事で動的にボタン内のテキストボックスのサイズを変更します。
- そしてそれをキャンバスパネルの左上のコーナーに配置します
簡単にまとめるとAttackTargetOptionと言う名のUIWidgetをブループリントで作成しました。このブループリントは戦闘画面で使用するはずなので、UCombatUIWidgetクラスから派生したブループリントクラスのCombatUIから呼び出されるはずです。
私はブループリントの勉強はホントの基本しかしていないので良くは知らないですが、UIからUIを呼ぶのは結構頻繁に行われる事なんでしょうか?
この辺のメリットは分かりません。
後、この呼び出す事の簡単さも良く分からないです。勉強すればデザイナー担当の人でも理解可能なんでしょうか?
<Step.8>
Step.8では以下の事を行いました。
- グラフ内で、新しい二つの変数を追加します。CombatUIのレファレンスタイプのTargetUIとGame Character のレファレンスタイプのtargetです。
- ボタンのイベントを作成します。Designer view から作成したボタンを選択してそのDetailパネル内のOnClickedをクリックします。
- そのボタンはAttack Target関数とAttackTarget関数をパスするためのTargetのレファレンス(このボタンが表しているターゲット)を呼ぶためにTargetUIのリファレンスを使用するようになります。
Step.8を今読み直してみると、ここに書かれている事しかやっていませんが、先週この文章だけを示されてブループリントで作成して下さいと言われても出来るかといえば出来ないですね。
もう一つ問題があるのが変数の決定です。ここでTargetUI変数を作成していますが、この変数CombatUIクラスから作成されています。Step.7で私はAttackTargetOptionはCombatUIから呼び出されると予想していますがAttackTargetOptionウィジェットがCombatUIを変数として保持する事になります。結構複雑になりそうですね。
<Step.9>
Step.9では以下の事を行いました。
ボタンクリックイベントのためのグラフは単純です。
- 割り当てられたtargetUIのAttack Target関数の実行。
- Targetのレファレンスをパラメーターとしてパス。
Step.8で述べている
3.そのボタンはAttack Target関数とAttackTarget関数をパスするためのTargetのレファレンス(このボタンが表しているターゲット)を呼ぶためにTargetUIのリファレンスを使用するようになります。
の部分ですね。
AttackTarget()関数は以下に示すようにUCombatUIWidgetクラスのメンバー関数ですので、
TargetUI変数から呼び出すはずです。そして攻撃する対象のキャラクターをパスする必要がありますので、Target変数をパラメーターとしてパスします。
ここではっきりしました。これをC++側のプログラミングを知らないデザイナーが作成する事は不可能です。なぜならAttackTarget関数がUCombatUIWidgetクラスのメンバー関数である事はC++側を見ないと分からないからです。
この辺りはプログラマーが担当しなければなりません。ではUIのデザインはどうするのでしょうか?
実際問題として誰かはUIをデザインしなければならないので、
の3つが考えられます。
3.が最も理想的に見えますね。まずデザイナーにプログラミングを強要しません。更にプログラマーにもデザインを要求しません。ので両方の才能を無駄なく発揮出来ます。更にデザイナーが紙と鉛筆でデザインしている間にプログラマーがC++の部分を出来ますので効率も良いです。しかし3.は完璧なwater fallデザインパターンですね。プログラミングの戦略としてはかなり良くないパターンです。デザイナーが作成したデザインが実現不可能な場合、別な問題が発生しそうです。
そこで3.の改良案です。
- デザイナーがUIのデザインの原案を紙と鉛筆で作成する。その間にプログラマーはUI作成に必要なプログラミングをC++で作成する。
- デザイナーが作成したデザイン(原案)を基に、プログラマーがブループリントでUIを作成する。
- 作成したUIをデザイナーがBPやUIウィジェット内のデザイングラフを変更して修正してデザインの原案に近づける。プログラマーは既に別な仕事を担当している。
もう完璧です。
この工程ならば、デザイナーにプログラミングのスキルを要求しません。しかしデザイナーがプログラミングのスキル(この場合BPのスキル)を持っていた場合、更に優れたデザインが出来ます。
次にプログラマーにデザインの要求もしないで済みます。プログラマーには高給を払って仕事して貰っているのにそのプログラマーに絵などを描かして時間を無駄に消費しないで済みます。
責任の分担も明確になります。原案のデザインは素晴らしいのに出来上がったUIが全然素晴らしくない場合、デザイナーがBPの知識が全くないので改良出来ないのか、プログラマーのBPまでのC++の設計が悪いのか(もしくはBPの設計が悪いのか)ではっきりします。
<Step.10>
Step.10では以下の事を行いました。
次にcharacter actionのためのパネルをメインのcombat UIに追加します。
これはcanvas panelに攻撃のための子供のボタン一つとターゲットのリストのためのvertical boxを追加したものです。
ここは100%デザインの分野ですね。前ならこれはデザイナーが担当すべきなのかプログラマーが担当すべきか分からなかったですが、今なら完璧に分かりますね。デザインそのものは既に原案としてデザイナーが作成していてそれを基にプログラマーがここではUIを足します。のでここはプログラマーの担当になります。
<Step.11>
Step.11では以下の事を行いました。
そしたら、ブループリントグラフ内で、ShowActionPanelイベントを実装します。
- Actionパネルを可能にするSet Visibilityノードに着くまでの実行のルートを決めます。
- ターゲットのリストを隠す別のSet Visibilityノードに着くまでの実行のルートを決めます。
ここでAttackTargetOptionウィジェットを呼ぶとばかり思っていたのですが呼びませんでした。CharacterActionsとTargetsをVisibleにセットしただけです。CharacterActionsはCanvas Panelの変数、TargetsはVertical boxの変数です。
<Step.12>
Step.12では以下の事を行いました。
Attackボタンがクリックされた時のブループリントグラフは大型なので小さく分けて見ていきます。
- 最初にAttackボタンのOnClickイベントを作成します。
- グラフ内で、前に追加されたかもしれない全てのターゲットオプションをクリアするためにボタンがクリックされた時、Clear Childrenノードを使用します。
Step.12 からはAttack ボタンはクリックされた場合の実装をブループリントからしました。ただし大変長いので分けて実装しました。Step.12では単にTargets内をクリアしただけです。
<Step.13>
Clear Childernノードから以下のノードを繋げました。
まずGetCharacterTarget()関数ですが、Step.4で以下のように実装されていました。
これで、敵のパーティのメンバーの配列を返すのですね。そしてそのメンバーが既に死んでいないかをHPからチェックしているのですね。この辺はどこまでC++で書いてどこからブループリントで書くかはプログラマー個人の采配によると思います。「この辺からデザインやUIが関係するからブループリントにしておこう。」とかなると思います。
<Step.14>
AttackTargetOptionウィジェットをTargets内に作成しました。うーん。AttackTargetOptionウィジェット内の変数、TargetUIとTargetsの値がまだアサイン(assign)されていません。
<Step.15>
されました。
やはりAttackTargetOptionウィジェットはcombat UIから作成されていながらそのcombat UIを変数として保持する複雑な形になりましたね。このタイプのソフトウェアデザインを行う場合何か注意する事があったと思うのですが忘れてしまいました。
Target変数が保持するキャラクターはGetCharacterTarget()関数がしっかり選んでくれています。
<Step.16>
全てのパーティメンバーの実装が終わった後にVertical boxであるTargets変数を表示します。
何で2回もTargetsを表示しないといけないのかと思ったのですが、Step.12のClearChildrenノードがTargetsを消しているみたいですね。
そんだけです。
これでStep.12 から始めたAttack ボタンがクリックされた場合の実装は完成しました。
<Step.17>
今度はHide Action Panel関数を作成しました。これはいつ必要になるのでしょう。
<Step.18>
以下に示すようにStep.17で作成した関数ノードをAttackTargetOptionウィジェット内から呼び出して追加しました。
攻撃が終了したらHide Action Panel関数を呼び出してcombatUIのCharacterActionsを消すのは当然ですね。
<Step.19>
現状のターゲットのキャラクターの名前をボタンに表示するために以下に示すコードをブループリントで作成しました。
これもこのままですね。コードを読めば一目瞭然です。
<まとめと感想>
もっと深く考察出来そうですが、初めて勉強したのでこのくらいにしておきます。今回の成果として、
UI作成時におけるプログラマーとデザイナーをどの作業にどのように分担するのかが解明しました。
1.デザイナーがUIのデザインの原案を紙と鉛筆で作成する。その間にプログラマーはUI作成に必要なプログラミングをC++で作成する。
2.デザイナーが作成したデザイン(原案)を基に、プログラマーがブループリントでUIを作成する。
3.作成したUIをデザイナーがBPやUIウィジェット内のデザイングラフを変更して修正してデザインの原案に近づける。プログラマーは既に別な仕事を担当している。