Levelを開く

とりあえず何かやったら記事にする。

自分が困った部分は他の誰かも困ってるはず。

というわけで今回はLevelを開く、というかLevelを移動する処理のお話です。

注意点として、今回はついにソースコードを書きます。

俺はBlueprintのみでゲームを作るんだ!という人にはあまり縁のない話になるかもしれませんが、こだわらなければそういう人にも意味がある話になる…かなぁ?

とにかく、今回はVisual Studioが必要になります。Expressでも問題ないらしいですが、私の環境はProなのでExpressでの動作は確認していません。

MacならXcodeが必要?こちらも環境を持っていないため動作確認していませんが、たぶん大丈夫でしょう。

ではプロジェクトを作成します。

いつもならBlankプロジェクトを作成するところですが、今回はBasic Codeプロジェクトを作成します。

名前は何でもいいのですが、LevelProjectとでもしておきましょう。

ue021.png

プロジェクトを作成するとEditorは立ち上がりません。その代わりにVisual Studioが立ち上がります。

VSが立ち上がったら構成が[Development Editor]になっていることを確認してから[ビルド] -> [ソリューションのビルド]を選択してビルドしてください。

少々時間はかかりますが、ビルドが成功したら[デバッグ] -> [デバッグの開始]を選択します。

いつものスプラッシュスクリーンが表示され、Editorが立ち上がり、いつものテーブルと椅子が表示されたらまずは成功です。

ビルドに失敗したりEditorが立ち上がらなかったりするようなら…申し訳ないですがForumなどで聞いてみてください。

まずはLevelの移動をやってみましょう。これは非常に簡単なのですが、今回の本題は実はちょっと違います。

[File] -> [Open Level]を選択してみてください。デフォルトコンテンツを含めているならMinimal_Default, StarterMapの2つの.umapファイルが見つかるはずです。

これがいわゆるLevelになります。[File] -> [New Level]で新しいLevelを作成すると.umapファイルが増えていきます。

Levelを移動するよくある方法として、プレイヤーがある場所まで来たら、というのがあります。

そこでまずは箱を置きます。左上の[Modes]にある[Geometry] -> [Box]でBox Brushを作成します。

[Details]タブでXYZの大きさをすべて100にして、この箱を机の上に置きましょう。

この箱はこのままでは当たり判定のある箱です。これをTrigger Volumeという、接触時などにいベンドを発行するためのボリュームに変換します。

[Details]タブの[Actor] -> [Convert]でTrigger Volumeを選択してください。

ue022.png

これにより、Box BrushはTrigger Volumeに変更されました。

変換前にゲームをプレイするとカメラ(正確にはDefaultPawn)が通り抜けられなかった箱が、変換後は通り抜けられるようになっているはずです。ついでに見えなくなってるはずです。

このVolumeに接触したらStarterMapに移動してみましょう。

[Details]タブの[Blueprint] -> [Add Level Events for Box Brush]の中から[Add OnActorBeginOverlap]を選択してください。

ue023.png

このイベントは他のActorがこのVolumeに重なった瞬間に発行されるイベントです。

LevelのBlueprintが表示され、そこにBox BrushのOnActorBeginOverlapイベントが配置されます。

このイベントの実行コネクタを左クリック&ドラッグして、コンテキストメニューが開いたらOpen Level関数を選択してください。

Open Level関数と繋がったらLevel Nameの項目に"StarterMap"と入力してください。

ue024.png

コンパイルし、実行してみましょう。

カメラを動かして机の上まで移動するとLevelの読み込みが走るはずです。

そして読み込みが完了したらStarterMapに移動します。

移動しないようでしたらLevel Nameにスペルミスがないか調べてみましょう。

さて、Levelを移動するだけならこれで完了です。何も困ることはありません。

しかし、多くの人が考えるはずです。Level移動時に何らかの情報を相手Levelに送ることはできないのか、と。

Levelをどの粒度で作成するかは製作するゲームの特性や製作チームの文化、製作のしやすさなどで変わってきます。

例えば『スーパーマリオブラザーズ』のようなゲームの場合、1-1, 1-2といったエリアごとにLevelを作成するのが一般的と思われます。

これに加えてタイトル画面なんかも別のレベルとして作成することになるでしょう。

もしもこのゲームがタイトル画面で難易度を調整できるゲームだったらどうなるでしょう?

対応方法の1つとしては、ゲーム開始時に移動するレベルを難易度別に作成し、選択された難易度に従って対応するLevelへ移動すればいい、ということになります。

難易度によって内容が大幅に変化するゲームならこれでもいいのですが、大半の、敵の耐久力や攻撃力が上がる、という程度であれば同一Levelで開始時に敵のパラメータを変更するだけの方が圧倒的に簡単です。

この場合、タイトルLevelから難易度情報をメインゲームLevelへ渡したいのですが、どうやらBlueprintのみではこの対応ができないようです。

自分がその方法を見つけられていないというだけの可能性はありますが、見つからなかったのでソースコードを書くことにします。

最初にLevelBPのOpen Levelノードを見てください。下に三角形が存在しているのがわかるでしょう。

ここをクリックすると隠されたパラメータが表示されます。

ue025.png

ここにOptionsという項目がありますが、ここに入力された文字列は移動先LevelのGameModeのOptionsStringというプロパティに保存されます。

ただし、このプロパティはBPから取得することができません。少なくとも、私は見つけられませんでした。

そこで、この文字列から必要な情報を取得するためにソースコードを書きます。

ソースコードを書くにはまず.cppファイルと.hファイルを追加しなければなりません。

自前で作成してプロジェクトに追加してもいいのかもしれませんが、Editorから実行する方が安全です。

[File] -> [Add Code to Project]を選択します。

ue026.png

すると親クラスを選べと言われます。作成するコードが継承すべきクラスをここで選択します。今回はGameModeを選択してNextボタンを押します。

ue027.png

次に名前を入れろと言われるのでクラスの名前を決定して入力、Create Classボタンを押します。今回はMyGameModeという名前にしましょう。

ue028.png

問題なく成功すればプロジェクトにMyGameMode.hとMyGameMode.cppが追加されています。

追加後、今から編集しますか?と聞かれます。はいを選んでもいいのですが、自分の環境だともう1つVSが立ち上がってしまいました。

どちらにしろビルドし直しなので、いいえを選んでEditorを閉じてしまいましょう。

VSに戻るとプロジェクトが外部から変更されて…と表示されるので、再読み込みします。

ソリューションエクスプローラでMyGameModeと入力すると前述の2つのファイルが見つかるので開きます。

まずはMyGameMode.hから編集しましょう。

AMyGameModeクラスを以下のように変更します。

UCLASS()

class AMyGameMode : public AGameMode

{

    GENERATED_UCLASS_BODY()

    virtual void BeginPlay() OVERRIDE;    // (1)

    UPROPERTY(VisibleAnyWhere, BlueprintReadOnly, Category = System)  // (2)

    FString SentMessage;                                                                                    // (3)

    UPROPERTY(VisibleAnyWhere, BlueprintReadOnly, Category = System)  // (4)

    int32 Difficulty;                                                                                                // (5)

};

追加された行にはコメント部で番号を振ってあります。

(1) はBeginPlayイベントが発行された際の処理を自分で書いた処理で上書きする、という意味です。

実際の処理はこのあと、MyGameMode.cppに書きます。

この関数は親クラスであるAGameModeも持っていて、このように明示しない限りは親が持っているこの命令が実行されます。

(2)~(5)は新しいプロパティを追加しています。

UPROPERTYという修飾子は下の変数がプロパティであることを明示しています。

()の中にはプロパティとしての性能がかかれています。

VisibleAnyWhereはどこからでも見ることができるという意味ですが、編集はできません。

BlueprintReadOnlyはBP上でGetすることができるがSetすることはできない、という意味です。

これらのパラメータはOptionsの文字列から取得することになるので、他の方法では書き換えができないようにしています。

Categoryはこれらのパラメータがどのカテゴリ内で見つけられるかを示しています。

ここではSystemカテゴリ内に存在するようになります。

追加されるプロパティは送られてきたメッセージ(SentMessage)と難易度(Difficulty)です。

次にMyGameMode.cppに以下のように追加します。

void AMyGameMode::BeginPlay()

{

    Super::BeginPlay();  // (1)

    SentMessage = ParseOption(OptionsString, TEXT("Message"));  // (2)

    Difficulty = GetIntOption(OptionsString, TEXT("Difficulty"), 0);       // (3)

}

AGameModeが持っているBeginPlay関数を独自の処理で上書きしています。

(1)はSuperクラスのBeginPlay関数を呼び出しています。

Superクラスというのはいわゆる親クラスです。つまり、AGameModeクラスのBeginPlay関数を呼び出しています。

結局呼び出すのかよ!と思う方もいるかと思いますが、C++では基本的にこのようにして処理を追加していきます。

OVERRIDEで上書きをする場合、基本的には親クラスの同名関数は呼び出した方がいいと思います。

(2)は送られてきたメッセージを文字列形式で保存しています。

OptionsStringはOpen LevelのOptionsに設定された文字列です。

この文字列は Key=Value の形にしておくとParseOption関数でValueを文字列として取得することができます。

また、(3)のように、整数に関してだけはGetIntOption関数で文字列ではなく整数をそのまま取得することができます。

もしもValueが整数でなければ、第3引数に渡した値が返ってきます。上記のコードなら0が返ってきます。

OptionsStringはKey=Valueの組み合わせを複数持たせることができますが、区切り文字として"?"を使用します。

例えば、送られてきたメッセージを"GetoutHere", 難易度が"2"なら、以下のように記述します。

Message=GetoutHere?Difficulty=2

もちろん、自前のフォーマットを用いてもかまいませんが、このフォーマットにしておくとParseOption関数が利用できるというわけです。

前述の編集が終わったらまたビルドして実行しましょう。エラーは出ないはずです。

Editorが立ち上がったら[File] -> [Open Level]でStarterMap.umapを開きましょう。

World Settingsを開いて[GameMode Override]のドロップダウンリストを出してみてください。

先ほど追加したMyGameModeが存在していますので、これを選択します。

ue029.png

選択したら[Selected GameMode]を開いてみてください。

Default PawnやHUDなどがグレーアウトして選択できないようになっているでしょう。

BPでGameModeを作成した場合はここでDefault Pawnなどを選択できるのですが、C++で追加されたGameModeでは選択できません。

特殊な設定を行いたい場合、C++のコードに追加するか、追加したGameModeを継承したBPを作成するかしてください。

さて、ではGameModeが受け取った情報を何らかの目に見える形にしてみましょう。

今回はSentMessageのみを利用してみます。

このLevelは最初からBeginPlayイベントに処理が追加されていますがそれらの処理の前にこちらの処理を入れましょう。

Sequenceノードを使うと、ある処理を実行後に別の処理を行う、ということができますので、BeginPlayからSequenceに繋げて、初期からの処理をThen1に繋げ、追加する処理をThen0から繋げます。

細かな設定方法は書きませんが、BPのノードは以下のようになります。

ue030.png

この処理は、SentMessageの内容により設定されている平行光源のカラーを変化させています。

SentMessageが"GetoutHere"の場合、ライトを赤くして5秒後にMinimal_Default.umapに移動します。

"ComeIn"の場合はライトカラーはそのままで、このLevelにとどまります。

それ以外のメッセージの場合はライトカラーを緑に変更し、このLevelにとどまります。

これが作成できたらこのLevelでゲームを始めてみましょう。ライトカラーが緑になるはずです。

次に再びMinimal_Defaultを読み込みます。

Box Brushを選択したらALTキーを押しながら移動してみましょう。Box BrushをコピーしたBox Brush_2が作成されます。

それぞれの名前を"BoxGetout", "BoxComeIn"に変更し、別々の椅子の上に配置してみましょう。

LevelBPを開いたらBoxGetoutのBeginOverlapイベントが存在するはずなので、これと同じものをBoxComeInでも作成します。

最後に、それぞれのOpen LevelノードのOptionsに以下の文字列を設定します。

Message=GetoutHere?Difficulty=2

Message=ComeIn?Difficulty=1

これで完成です。Minimal_Defaultでゲームをプレイしてみましょう。

まずはBoxGetoutが置かれた椅子に向かってください。Boxに接触するとLevelが読み込まれ、移動します。

移動した先はライトが真っ赤な状態になっているはずです。あとは5秒経過するのを待ちましょう。

5秒経過するとまたMinimal_Defaultに戻ってきます。今度はBoxComeInに接触してみます。

再びLevelが移動しますが、今度はライトカラーは正常です。5秒経ってもLevel移動はしません。

というわけで、今回はLevel移動とLevel間での情報のやり取りについてやりました。

やってることは単純なのですが、どうしてもソースコードを書かなければならなくなってしまいましたね。

まあ、非プログラマでも追加自体は簡単だと思いますし、応用もしやすいとは思います。

もし、もっと簡単な方法があるようでしたら教えてください。

出来ればBPのみで完結する方法がいいのですがね…