UE4の勉強記録

UE4の勉強の記録です。個人用です。

Unreal Engine 4.xを使用してRPGを作成する」の足りない部分を作成する 村人の会話を管理する

f:id:kazuhironagai77:20201011202512p:plain

<前文>

I was worry about…は間違いなのか?

何処かの総理大臣が英語でツイートした時の文言らしいですが、ネットで英語が間違っていると叩かれているそうです。この文章の何処が間違っているのかも二転三転していて、最初私が聞いたのは「be + worriedじゃなくてworryが正しい。」と言う意見だったんですが、それが間違いでworryでもbe worriedでも正しい事が分かると今度はwasが間違っていると言う事になりました。

私は、この総理大臣を庇いたい気持ちは全くないんですが「C-何とかに罹ったと聞いて本当に心配しました。」と素直に解釈しましたし、英文法のサイトで調べてもそういう意味に取れると思います。

何で人の英文にケチつける人は自分では調べもしないんですかね。それと、ここで大切なのは自分の気持ちを伝える事で100%正しい英文を書く事じゃないですよね。日本語が分からない人に日本語で大丈夫ですか?と言っても通じないから英語をしゃべる事が大事なんでしょう。文法的にちょっとくらい間違っていたとしても気持ちが伝わる事の方が大切じゃないんでしょうかね。

私も10年アメリカに住んでいたので普段の英語には問題ないですが、論文の英語はアメリカ人の教授がキッチリ直してくれました。アメリカ人とまったく同じ英文が書けるようにはとうとう成らなかったです。

後、英文法盲信君達は勘違いしているけど、受験英語の文法を幾ら勉強してもアメリカ人から見て文法に間違いがない英文は書けるようにはなりませんよ。平均的な知性があれば英文法極めた大先生達がちっとも英語でTwitterしてないのを見れば気が付くんですが。英語でtwitterすればfollower数だって何倍になりますよね。更にアメリカ人の英語の文法の間違いなんか指摘出来たら、日本人のfollower数も爆増しますよ。でもしないと言う事は…、そう言う事なんですよ。

じゃあ、幾ら勉強しても英語をアメリカ人のように書くのは不可能なのかと言うと、そうでもないみたいなんです。先週のvtuberの炎上で英語圏で有名なvtuberの切り抜き翻訳サイトが中国人が運営していた事が判明したんですが、その翻訳、私はアメリカ人が書いたとずっと思っていました。その後、あまりに中国寄りな意見のため、アメリカ人のファンからそのサイトは総攻撃されたんですが、英語の間違いを攻撃している人は一人もいませんでした。それから推測するとその翻訳を担当していた中国人グループは完璧な英語を取得していたと思われます。

中国は科学技術で世界の最先端に追いついただけじゃなくて、語学などの文系の分野でも日本がまだ知らない特別な学習方法を編み出した可能性があります。更にその学習方法に基づいて勉強すればネイティブと同じレベルまで上達可能みたいです。

先週、vtuberの中国での炎上が収まらなかったらもっと深い考察をやると言いましたが、まだ考えがまとまっていないのと別な内容でまとめたい事があるのでそっちを先にする事にしますが、必ずまとめます。

それでは今週の勉強を始めます。

<本文>

先週やり残したデザインパートからやって行きます。

Design Part

  1. 村人と会話出来る様にする
  2. 魔法の拾得方法について
  3. 戦闘システムのボタンのデザインを統一する
  4. 実際のゲーム内の町の地面をデコボコにする

Programming Part

  1. 夜と昼の作成の続き
  2. 村人の会話システムの構築

1. Design Part

1.1 村人と会話出来る様にする

ゲームデザインの観点から考えると例え村人と会話出来たとしても面白くなければ意味がないと思います。多くのRPGで村人は単なる愚か者として描かれています。そんな人達と会話する意味があるんでしょうか?

私が作成するゲームでは村人は主人公と比較して知性が劣っている訳ではなく主人公とは人生の目的が違う人達です。

以下のタイプを考えています。

1. 詩を読む村人

  • 詩人になりたくていつも詩を作成している人です。
  • 話しかけると作成した詩を披露してくれます。
  • 更に主人公に詩の評価を聞いてきます。高評価なら喜び、低評価だと悲しみます。

2. 占い師

  • この世界では霊感が備わった人は守護精霊からの警告が占いの形で聞き取れるとの信仰があります。それを実践する人達です。
  • 話しかけると主人公の未来を占ってくれます。

3. 科学者の卵

  • 再現性を重んじ、繰り返し再現出来る事に価値を見出します。この世界では、魔法が強力なため科学技術はあまり発展していません。
  • 話しかけると、科学の基礎に発展しそうな考察を披露してくれます。

4. 天気予報の達人

  • 村人は基本的に農民なので天気の予測は非常に大切です。
  • 話しかけると今年の雨量、暑さ、寒さなどの予測を教えてくれます。

5. 政治評論家

  • この世界の王たちも政争に明け暮れています。
  • 話しかけると今の政治状況について解説してくれます。

6. 山の達人

  • この世界の村人は、春は山菜、秋はきのこ狩りをするのが普通です。山の達人はそれらを採る達人です。
  • 話かけると、山の情報について教えてくれます。

7. ビジネスマンの卵

  • どうやったらお金持ちになれるのかいつも考えています。
  • 話しかけると、今考えているビジネスについて説明してくれます。

8. 退役した戦士

  • 退役した戦士も農民になって暮らしています。
  • 話しかけると、戦闘に有利な情報や退役後にのどかに暮らす方法などについて教えてくれます。

9. 魔法研究家

  • 魔法について研究している村人もいます。
  • 話しかけると、魔法についての情報を教えてくれます。

10. 村長

  • 村のリーダーです。村の食料、治安、水などを管理しています。
  • 話しかけると、村のビジョンについて語ってくれます。

11. 村長の腰ぎんちゃく

  • 村長の機嫌を良くする事だけが生きがいです。
  • 話しかけるとどうして腰ぎんちゃくとして生きているのか教えてくれます。

12. 村長の娘

  • 村人から姫と呼ばれています。理想の女の子を演じるのに疲れています。
  • 話しかけると本音を教えてくれます。

13. 保安官

  • 村の治安を守っています。村で武器の使用が許可されている唯一の人です。
  • 話しかけると仕事を頼まれます。保安官に頼まれた仕事は村にいる限りやらなければいけません。因みに料金はもらえません。

14. 旅人

  • 旅人は主人公だけではありません。色々な理由で旅をしている人も村に宿泊しています。
  • 話しかけると、世界の旅の仕方について教えてくれます。

15. ハンター

  • 村の外には凶悪なモンスターが出現する事があります。その時には、村人はハンターを呼んで退治してもらいます。ハンターは高額な報酬を貰えるのでみんなの憧れの職業です。
  • 話しかけると、バカにされます

村人の会話の内容はこんなもので言いと思います。しかしどうやって管理しましょうか。以下に問題点を書きます。

  • 村は一か所ではありません。○○村の詩人。の様に管理すべきでしょうか?それとも…
  • 村人の会話も単一では面白くありません。色々な条件が変化する事で会話の内容が変化するようにしたいです。

このゲームは色々なレベルを移動するので、特性はGameInstanceクラスで管理する必要があります。しかし一々GameInstanceに変数を作成していたら管理出来ません。

以下に現在全てのNPCに使用しているNPC_Oldmanを示します。

f:id:kazuhironagai77:20201011203428p:plain

中のBPを見てみると既に変数NPC_OldmanをRPGGameInstanceBP内に作成しています。

f:id:kazuhironagai77:20201011203517p:plain

f:id:kazuhironagai77:20201011203534p:plain

何でこのクラスのための変数は、GameInstanceクラスのUE4C++側じゃなくてBP側に作成したんですかね。覚えていませんね。

NPCの会話のシステムの実装をどうやったのか忘れていました。それをまず復習します。

f:id:kazuhironagai77:20201011203607p:plain

Keyboardのeを押す事でDoSomething Eventが発生します。

Event DoSomething はThirdPersonCharacter内で実装されていました。

f:id:kazuhironagai77:20201011203800p:plain

あれ、これ見るとRPGGameInstanceBP内の変数NPC_Oldmanは必要無いですね。

f:id:kazuhironagai77:20201011203818p:plain

NPC_Oldmanがどこに使用されているか調べたら昔作成して今は使用していない箇所のみで使用されていました。確かに要らない変数です。

外します。

f:id:kazuhironagai77:20201011203835p:plain

テストします。

きちんと動きました。

f:id:kazuhironagai77:20201011203903p:plain

セリフの管理状況を見てみます。

単にNPC_Oldmanクラス内の変数で管理しているだけでした。

f:id:kazuhironagai77:20201011203924p:plain

うーん。実は以下のPlugInを購入してセリフの管理を一括で行おうか迷っています。

f:id:kazuhironagai77:20201011203947p:plain

取りあえず今回は使用しないで作成してみます。

この老人と会話するシステムを作成します。

以下に示したように作ってみました。

まず、NPC_Parentウィジェットの変数、NPCOldmanのセリフを以下の様に変更します。

f:id:kazuhironagai77:20201011204006p:plain

f:id:kazuhironagai77:20201011204014p:plain

0は質問です。

次にInteger変数とText変数を持つStructを作成します。

f:id:kazuhironagai77:20201011204032p:plain

名前はAnswerCommentManagementとしました。

f:id:kazuhironagai77:20201011204049p:plain

このStructを使用してNPCOldmanのセリフ0に対する答えを作成します。

f:id:kazuhironagai77:20201011204106p:plain

f:id:kazuhironagai77:20201011204114p:plain

ここで「はい、そうです。」を選択した場合はJumpToCommnetに1がセットされているのでNPCOldmanのセリフ1が次に表示されます。「いいえ…」を選択した場合はJumpToCommnetに2がセットされているのでNPCOldmanのセリフ2が次に表示されます。

Oldman_Welcomeウィジェット内に以下に示した実装を行います。

f:id:kazuhironagai77:20201011204221p:plain

因みに以下のようなコメントが表示されているんですが、

f:id:kazuhironagai77:20201011204243p:plain

Oldman_WelcomeウィジェットのParentクラスはNPC_Parentウィジェット

f:id:kazuhironagai77:20201011204301p:plain

問題なく継承出来ているみたいですが。widgetからwidgetを継承出来ないとはどういう事なんでしょうか?取りあえずはこの警告は無視して先に行きます。

Oldman_Welcomeウィジェット内で実装されたコードでvertexAnswer(以下に示した四角の部分)内に作成されるボタンウィジェットは、

f:id:kazuhironagai77:20201011204321p:plain

AnswerButtonウィジェットで、

f:id:kazuhironagai77:20201011204339p:plain

OldmanAnswerFor0に含まれているコメントをボタン上に表示します。

f:id:kazuhironagai77:20201011204410p:plain

更にそのボタンをクリックすると

f:id:kazuhironagai77:20201011204428p:plain

Oldman_WelcomeのvertexAnswer内のボタンを全部消した上に、TextBlock_OldmanDialogに表示されているセリフをNPCOldmanのセリフ0からセリフ1もしくは2に変更します。

テストしてみます。

f:id:kazuhironagai77:20201011204446p:plain

はいそうです。を選択します。

f:id:kazuhironagai77:20201011204503p:plain

セリフが変わりました。

もう一回話しかけて、今度は「いいえ…」を選択しました。

f:id:kazuhironagai77:20201011204522p:plain

ここまでは出来ています。

最初の構想からはかなり稚拙な出来になってしまいました。

今の状態では、話しかけるたびに最初の状態に戻ってしまいます。GameInstanceクラスに何も保持する必要もありません。しかしこの部分が出来ないとこの先も何も作れないので取りあえずこれでやってみます。

今度はもっとセリフを増やしてみます。

NPCOldmanのセリフ1に対する返答をOldManAnswersFor1に作成します。

f:id:kazuhironagai77:20201011204539p:plain

それに対する返答もNPCOldmanに追加します。

f:id:kazuhironagai77:20201011204558p:plain

これらの会話が実行されるようにOldman_welcomeウィジェットの実装も少し変更します。

f:id:kazuhironagai77:20201011204621p:plain

テストします。

はいそうです。を選択します。

f:id:kazuhironagai77:20201011204639p:plain

「はい。そうです。」を選択します。

f:id:kazuhironagai77:20201011204656p:plain

新しい選択ボタンが表示されました。

「ビカルト…」を選択します。

f:id:kazuhironagai77:20201011204712p:plain

最初に戻って「珍しい魔石…」を選択してみました。

f:id:kazuhironagai77:20201011204737p:plain

出来てますね。

1.2 NPC会話システムの考察

一応NPC一人分の会話を作成する事は出来ましたが、このやり方では沢山のNPCの会話を管理する事は出来ません。

現在、非戦闘時の会話やコメントは、NPC_Parentウィジェット内で一括管理しています。これは他の言語に翻訳したり、会話の問題を発見したりしやすくするためには絶対必要です。

しかし以下に示すように、NPC一人で、NPCの会話で変数を一個、その解答のための選択ボタンの作成にそれぞれ一個の変数、

f:id:kazuhironagai77:20201011204804p:plain

を作成してったら変数の数が膨大になってどれがどれを指しているのかを管理する事は不可能になります。

色々考えた結果、以下の方法ならこのNPCとの会話の管理が可能ではないかと思いました。

1.1の結果を踏まえると、NPCとの会話はNPCからの質問、それに対するプレイヤーの回答部分に分かれます。更にプレイヤーの回答は何個もあり、その回答によって次のNPCの答えも変わってきます。

これを表にまとめると以下の様になります。

f:id:kazuhironagai77:20201011204823p:plain

これを見て、思ったのですが、NPCの会話はアドベンチャーゲームブックにそっくりです。

中学生の時、アドベンチャーゲームブックを作成するのが趣味の同級生がいて、休み時間によくその友達が作成したアドベンチャーゲームブックで遊んだんですが、どうやってその友達がアドベンチャーゲームブックを作成したのか私は全く分からなかったです。ある日作り方を教えてくれたのですが、それは以下の方法でした。

  1. まず真っ新なノートに目次を振ります。
  2. 次に1ページ目に文章を書いて、aを選択するなら15ページへ、bを選択するなら8ページへと書きます。
  3. その次に15ページに行って続きを書く。それをただただ繰り返せばアドベンチャーゲームブックは完成すると教えてもらいました。

後年、Computer scienceのクラスでalgorithm とは何かを教わった時にこの事を真っ先に思い出しました。彼はalgorithmの概念そのものを自分で発見していたんです。

話がそれ過ぎました。元に戻します。

アドベンチャーゲームブックの作成方法と全く同じ手順でNPCとの会話のデータテーブルを作成しようと思います。完成したデータテーブルはa村の詩人とか、b町の村長と名付けます。そして村ごとにフォルダーを作成して管理します。その村ごとのフォルダーはNPCフォルダーに入れて置けば全部のNPCの会話を一個のフォルダーで管理できるわけです。

多分、本格的なRPGはdatabaseを活用してNPCの会話を一括管理しているのでしょう。しかしこのゲームにdatabaseまで活用するのは重すぎます。まず私はMySQLしか扱った事はないので、このゲームのためだけにDatabaseまで勉強し直すのは正直きついです。それにdatabaseはサーバーで管理しないといけないのでUE4とサーバーの関係も勉強しないといけなくなります。今回はデータベースの活用はパスします。

そういえば去年の一時期、FirebaseとUE4を繋げてサーバー管理はfirebaseに任せようとfirebaseの勉強を熱心にした時期がありました。FirebaseのTutorialは一通り勉強したんですが、そのtutorialで使用されているsample codeがJavaScriptで普通に使用されている方法なのかFirebase独特の関数のための使用方法なのかが良く分からなかったんです。これはJavaScriptをもう一回勉強しないとかやってる内に一年間の無料サービス期間が過ぎてしまいました。更にfirebaseを使用していた人から使用料が結構高額になるから有料のサービスでないと使うだけ赤字になると言われて結局Firebaseの勉強を止めてしまいました。

Epic game社が無料で提供しているオンラインサービスは結構凄いらしいので、具体的にどんなサービスを提供しているのかは分からないですが、いずれは勉強しようとは思っています。

1.3 最初の村にいるNPCの老人の会話用データテーブルの作成

1.2でまとめた方法でもう一度、NPCの会話システムを作成してみます。

まずStructを作成します。今回は試しなのでBPで制作します。

f:id:kazuhironagai77:20201011205027p:plain

このStructを元にしてDataTableを作成します。

f:id:kazuhironagai77:20201011205054p:plain

f:id:kazuhironagai77:20201011205101p:plain

f:id:kazuhironagai77:20201011205109p:plain

1の質問に対する回答は2つ用意されています。一つ目を選択した場合は、2のコメントが表示され、2つ目を選択した場合は、3つ目のコメントが表示されるようにコードを書いて行きます。

f:id:kazuhironagai77:20201011205130p:plain

データデーブルの方も変更しました。

f:id:kazuhironagai77:20201011205146p:plain

まず、NPCの質問は長いので行替え出来る様にしました。Ctl+Enterでは行替え出来できないので、このサイトを参考にして{nextline}を改行したい個所に挿入します。

そして以下のようなコードを通して表示します。(Inputの値はCtl+Enterです。)

f:id:kazuhironagai77:20201011205211p:plain

更にJumpToCommentの番号を1ずつ減らします。

f:id:kazuhironagai77:20201011205228p:plain

これで試します。

f:id:kazuhironagai77:20201011205251p:plain

はい。を選択します。

f:id:kazuhironagai77:20201011205315p:plain

もう一度、会話に戻って今度は「いいえ…」を選択しました。

f:id:kazuhironagai77:20201011205337p:plain

こっちのコメントは行替えしていませんでした。直します。

f:id:kazuhironagai77:20201011205401p:plain

会話をもう少し複雑にします。

f:id:kazuhironagai77:20201011205421p:plain

f:id:kazuhironagai77:20201011205431p:plain

テストします。

f:id:kazuhironagai77:20201011205459p:plain

上を選択します。

f:id:kazuhironagai77:20201011205518p:plain

下を選択します。

f:id:kazuhironagai77:20201011205533p:plain

複雑にしても対応していますね。

これで単純なNPCの会話システムは作成できそうです。

1.4 次の村にいるNPCの老人の会話の作成

これは、セリフだけ別に作成して残りは同じオブジェクトを使用したいです。

まず、村の名前を保持したEnum、villageNameを作成します。

f:id:kazuhironagai77:20201011205556p:plain

次にNPC_OldmanクラスにtypeがvillageNameである変数を作成します。名前はvillageNameとしました。

f:id:kazuhironagai77:20201011205614p:plain

f:id:kazuhironagai77:20201011205620p:plain

そしてこの変数の目の部分をクリックして目が開いている状態にします。

この目が明いている状態だとこの変数はPublicになるんですが、私はこの意味がずっと分からなかったんです。しかし最近やっと分かりました。のでここに解説を残しておきます。

BPに作成した変数はGetter、Setterが自動で作成されるのでpublicだろうがprivateだろうが他のBPから簡単にアクセス出来ます。つまりpublicにしてもprivateのままでも同じなんです。なのに何でこんな機能が付いているのかと言うと、変数をPublicにするのが目的ではなくて、それぞれのインスタンスに別な初期値をセット出来るようにする事が目的だったんです。

確かに以下に示す様に目の解説を見るとそれぞれのインスタンスで編集可能と書かれています。

f:id:kazuhironagai77:20201011205638p:plain

UE4エディターからNPC_Oldmanクラスから作成したインスタンスを選択して

f:id:kazuhironagai77:20201011205708p:plain

そのdetailを見てみると、

f:id:kazuhironagai77:20201011205833p:plain

編集可能な変数としてVillageNameが表示されています。

これが目が開いている事の目的だったんです。

因みにUE4C++におけるUPropertyのSpecifierのEditAnywhereが同じ働きをしますね。(最初、EditInstanceOnlyと同じかと思ったんですが、目が開いた状態の変数の値はInstanceだけでなくBP本体でも編集出来るのでEditAnywhereと同じでした。)

以下に示すように、NPC_Oldmanクラスから2体のインスタンスを作成してそれぞれのvillageNameをFirstVillage、SecondVillageとします。

f:id:kazuhironagai77:20201011205910p:plain

f:id:kazuhironagai77:20201011205918p:plain

f:id:kazuhironagai77:20201011205925p:plain

これで同じNPC_Oldmanクラスでもそれぞれのinstanceで違うセリフをしゃべる土台が出来ました。

今度は、SecondVillageに住む老人用のセリフを作成します。

f:id:kazuhironagai77:20201011205941p:plain

データの要素はFirstVillage_Oldmanと全く同じですが、セリフとその回答のための選択ボタンは全然違います。

f:id:kazuhironagai77:20201011210019p:plain

それぞれのinstanceで違うセリフを表示するための実装は以下に示したように行いました。

NPC_Oldmanクラスでplayerが操るキャラがボックス内に侵入すると、そのNPC_Oldmanクラスのinstanceが持っているVillageNameの値をThirdPersonCharacterクラスの変数、villageNameにコピーします。

f:id:kazuhironagai77:20201011210048p:plain

キーボードのEがクリックされた場合、ThirdPersonCharacterクラス内でOldmanWelcomeウィジェットが作成されますが、その時にVillageNameの値がOldmanWelcomeウィジェットにパスされます。

f:id:kazuhironagai77:20201011210107p:plain

OldmanWelcomeウィジェットではVillageNameの値によって別々のデータテーブルから会話のためのセリフを読み込みます。

f:id:kazuhironagai77:20201011210125p:plain

この部分の実装で、何でswitchを使っているですか?polymorphism使えば一行で書けるじゃないですか?と思う人がいるかもしれません。私もそう思っていました。でも出来ませんでした。DataTableから親クラスや子クラスが作れなかったんです。最初データテーブルでOldmanと言う親クラスを作成してそこから子クラスであるFirstVillage_OldmanとSecondVillage_Oldmanを作成してpolymorphismで一気に行こうとしたら出来ませんでした。もしかしたら私が知らないだけで出来るのかもしれませんが、そもそもDataTable自体がクラスじゃないですし、出来ないと考える方が理に適っていますので、あまり調べずに上記の方法で実装しました。

PolymorphismというかInheritanceはオブジェクト指向言語の悪い点として悪名を轟かせていますが、無ければないで非常に不便です。この例で言えば、switchの分岐した先のコードは全く同じですが、compilerがFirstVillage_oldmanとSecondVillage_Oldmanが同じ形式のデータテーブルと認識してくれないので、全く同じコードを2回書いています。最終的には村の数だけこの分岐は増えるので大変なスパゲティコードになってしまいます。

これで完成したはずなのでテストしてみます。

まず左の老人に話しかけます。

左の老人はVillageNameの値がFirstVillageなので今までと同じセリフで会話してくるはずです。

f:id:kazuhironagai77:20201011210216p:plain

していますね。

今度は右の老人に話しかけます。

f:id:kazuhironagai77:20201011210234p:plain

はい。全く違うセリフを言いました。SecondVillage_Oldmanデータテーブルからのセリフです。

出来ました。

もう時間がなくなってしまったので今週はここまでとします。

2. まとめと感想

今週はNPCの会話システムの作成の途中で終わってしまいました。NPCの会話システムの構築は、思っていたより大変です。来週も引き続きNPCの会話システムを作成していきます。