LoginSignup
2

More than 3 years have passed since last update.

ハズレ無し!必ず当たる!

Last updated at Posted at 2020-12-09

UE4 アドベントカレンダー10日目の記事です

めちゃくちゃいかがわしいタイトルにしてみた。
記事のネタどうしようかと悩んでいたとき、ふっと思い出したのが、幼児雑誌の付録に入ってた数字を使ったマジック。
マジックは訓練された人間がやったり、こっそりタネを仕込んであったりするものだと思っていたので、その当時しくみに気づいたときはものすごく感動したのを覚えている。UE4みたいな SUGOI ENGINE でこんなネタやるのはなんだか気が引けるけど、まぁ はっぴーほりでー ということで。

数字を当てる

このマジックは4枚のカードを使う。
それぞれのカードには、それぞれ 1~15の中からある法則によって選ばれた 8つが書かれている。

誰かひとりの被験者に 1~15 の数字を声に出さずに頭の中で選ばせる。
次に4枚のカードを1枚づつ順番に見せて、選んだ数字があるか尋ね、
「ある」か「ない」で答えてもらう。
4枚のカードを確認し終えたら、カードに応じた「ある」「なし」を集計すると、なんと
頭の中で選んだ数字をピタリと言い当てられるというもの。

作ってみた

クリスマスイベントの余興にでも、と思ってさっそくUE4で作ってみた。
数字で普通に作ったらすぐにネタバレして面白くないので数字をイラストに変えてみた。

スタート画面はこんな感じ
fig01.jpg
例えば「みかん」に決めたとする
fig02.jpg
↑ある
fig03.jpg
↑ない
fig04.jpg
↑ない
fig05.jpg
↑ある
fig06.jpg
ドラムロール的演出からの・・・
fig07.jpg
確定! という具合。

SPEC

  • エンジンVer 4.25.4
  • UMGを使用
  • テクスチャ2枚を1つのMaterialで扱う
  • ほぼWidgetブループリントOnly

Texture

テクスチャを用意する 1024x1024 が 2枚 イラストひとつが 256x256
tex_201210a.png
tex_201210b.png
素材は いらすとやさんです ありがとうございます。
この32種から毎回ランダムで15種を選んで使う。
テクスチャを作る手間を省きたい方は上のPNG画像を右クリッ・・・(ry

Material

マテリアルはIndex番号でキャラを取り出せるようにしてある。
fig00.png
Index番号は 0~31 だけど、計算量をこれ以上増やしたくなかったので、ページ切り替えの判定はブループリント側でやる。
今回扱うのが 32キャラで、たまたま 2枚で済んでるので、マテリアル内に直接配置している。ゲームのプロジェクトでこのように都合よく最大数が決まっている例はそれほどないので、最終的に数が増えるのを想定するなら、テクスチャパラメータにしておきたい。

Widget Blueprint

下準備

今回はこの32種のキャラクターを常駐させて扱うので、さっさと Event Pre Construct で配列に仕込んでしまう。
fig08.png
Construct Object from Class ノードで Widgetの Image を生成しつつ、それを配列 source_Images に追加。
一気に、マテリアルセット → ブラシサイズ(表示サイズ) → GetDynamicMaterial → パラメータをセット
まで。
これで好きな時にImage型の配列からキャラを取り出して使うことができる。

ForLoop が終わった後の関数は配列の初期化用
fig10.png
4種のカードを順番に見せて確認するんだけど、その段階をフェイズと呼ぶことにして、その順番をランダムにするために、card_Order という配列を用意。
同様に image_Order というInt型の配列は、登場するキャラをシャッフルするためのもの。
最初に用意したImage型の配列をシャッフルするより、データ量の小さな配列を作ったほうが軽いしデバッグしやすいかなという目論見。シャッフルして 0 ~ 14 までを使う。

スタート画面

キャンバスに以下のパーツをレイアウト。
fig12.png
繰り返し遊べるように、カスタムイベントを用意。
Event Constructはそのカスタムイベントを呼び出すだけ。
fig11.png

ここでいったんレベルに表示してみる。
FileメニューからNewLevel を選択。
fig15.jpg
fig14.jpg
新しいレベルが開いたら、Blueprintsメニューの Open Level Blueprint を選ぶ。
fig16.jpg
グラフには Create Widget ノードで 一度コンパイルして保存したWidgetをセットしてAdd to Viewportする。
fig17.png
マウスカーソルを常時表示したいので、GetPlayerController が持つ ShowMouseCursorTrueにする。

再生してみるとこんな感じに。
fig13.jpg
WrapBoxパネルを水平方向に5つ並んだら改行するように幅を調整する。
今回Source_Images(Image型の配列)が保持しているキャラのブラシサイズを224x224 にしたのと、SetPaddingノードで左右に10ずつ余白を入れてるので、1220 でちょうどいい感じになった。

ボタンをクリックするとチェック開始にしたいので、On Clicked のイベントを用意する。
UMGの編集モード(Designer)にして、Buttonを選んだ状態でDetailタブの一番下に並んでいる緑のボタンをクリックする。
fig18.png
fig19.png

チェック開始

まずはスタート画面を片付ける処理をOnclickedイベントにつないでいく。
fig20.png
次にAddChildするとき邪魔なので ClearChildren でWrapBoxに追加したものをクリアする。
ここでInt型の変数を2つとImage型の配列を新しく追加する。
phaseCountは進行管理用、resultは「ある」か「ない」の集計用。

この後表示用の処理が続くんだけどキリがいいので、いったん止めておいて、関数とキャンバスの用意を進めます。

マジックの基本は最初に選んだ15種のキャラから、8種を配置して「ある」か「ない」かチェックしてもらうこと。
そのキャラたちを配置するとても重要な関数 MakeCheckcard を用意する。ちょっと大きくなったので2枚の画像に分割。
fig24.png
引数はCanvasPanel型とInt型の2つ。
fig25.png
を使っているのが肝。種明かしはあとでやるとして、まず
この関数ではInt型の引数であるMaskからカード番号の 0~3 が入ってくるので、そこに応じて、必要な 8種をチョイス。
そしていい感じにポジションとサイズ、アングルの3要素にランダム要素を加えてキャンバスに配置している。
tmp_LocationIndex は Int型のローカル変数。
アングルとサイズはおまけ。キレイにまっすぐ並んでると面白くないので。ちょっと試してみたいだけなら不要。
SetPosition がちょっとだけややこしい。
これは連番のものを、中央揃えするための計算をしつつ、不規則さを加えてる。ここもWrapBoxを使えばもっとシンプルにできる。

左から右下にかけて並べる場合
基準となる座標 - ((表示個数 - 1) * 表示間隔) / 2
から並べるといい感じに中央揃えできるんだけど、キャラのピボット位置(Alignment)が左上なので、その分の補正も加えて最終的に表示開始のX座標が360になった。

今回基準となる座標 = 画面中央なので、1920 / 2 = 960
ここに 4個並ぶので
960 - ((4 - 1) * 300) / 2 = 510
表示間隔の半分が 300 / 2 = 150
よって、510 - 150 = 360

キャラを選んで配置する関数ができたので、次の関数を作る。

フェイズに応じたカード番号を取り出す関数で、
事前にシャッフルしておいた card_order の配列から値を抜き出すだけの簡単なお仕事。
fig21.png
引数はないのでPure型にして使うことにする。
fig22.png

次に、マクロを用意。
fig23.png
べき乗のノードはInt型では使えないのでFloat型にキャストして計算。

これらをつなぐカスタムイベントを作る。
fig26.png
追加したキャンバスパネルを表示して、さっき作ったチェック用のカードにキャラを並べる関数を呼び出す。
引数には、カード番号を取り出す関数とマクロをつなぐ。

あとは、表示用のキャンバスパネルと、テキスト、「ある」「ない」のボタン、そしてナビテキストをキャンバスに追加。
fig27.png
実は「ある」「ない」のボタンは、まとめて表示/非表示を行いたいのと、レイアウトの調整がしやすいように、HorizontalBoxに収納してある。

レイアウトした時点では Visibility設定を "Collapsed" にしておく。
このキャンバスパネルは再利用するので、子階層には何も置かないようにする。ClearChildren したときに一緒に消えてしまうから。

ナビテキストは 進み具合を表す表示で、横着してTextBlockでやる。
仕組みは関数で 記号の ○ と ● を使ってゲージぽく切り出す。
fig29.png

そして途中だったスタート用の OnClickedイベントを仕上げる。
カードをレイアウトして表示するカスタムイベントの呼び出しと、キャンバスに置いたその他のパーツ、ナビを表示。
fig28.png

次のカード

「ある」か「ない」のボタンをクリックしたときのイベントを、スタート時と同じように作っていく。
fig30.png
まず「ある」と答えた場合、カードの順番に応じた値を Result変数に加算する。
fig31.png

どちらを選んでも次のカードに切り替えたいので、いったんキレイにする。
fig32.png

ここでフェイズが最終段階じゃなければ、フェイズカウントをひとつ進めて引き続き次のカードの表示へ。
4枚目だったら結果発表という分岐になる。
fig33.png

結果発表ルーレット

結果表示用のパーツをキャンバスに追加。
fig34.png
レイアウトした時点では Visibility設定を "Collapsed" にしておく。
SizeBoxを選んだのは、中に入れたものが自動でSizeBoxの大きさまでストレッチされるのと、
一つしか同時に表示しないのが理由。

次にこのSizeBoxへキャラを表示するカスタムイベントを用意する。
関数でも構わない。なんとなく同じグラフ内の方が作業しやすかったので。
fig35.png
基本的にキレイにして追加するだけの処理。

すぐに結果表示してもつまらないので、ルーレットのようにもったいぶって表示したい。
そこでこのイベント処理。
fig36.png
新たに 表示間隔をコントロールする Float型の変数を追加してループさせてる。
頻度が低く必要な時しか動かさない場合は、Event Tick ではなく Set Timer by Eventノードをオススメしたい。

確定表示

最終結果表示用にテキストをキャンバスに追加。
fig37.png
テキストの内容は「これです!」と「・・・」の2つ。レイアウトした時点では Visibility設定を "Collapsed" にしておく。
このマジックは普通にやると問題なく当たるんだけど、ウッカリすべて「ない」を選択してしまえるので、一応例外対策として「・・・」を表示する。
すべて「ない」ということは、最初の15種から選んでみたものの、ふいに何だったか忘れてしまった場合か、そもそもうまく選べていなかったか。勘違いしていたり・・・とか。

ルーレット処理が終わったあとで
fig38.png
結果表示のSizeBoxは残して、テキストだけを入れ替える。
実は、Result には計算の都合上必ず 1以上の値が入ることになっている。
配列に積んだキャラ絵を取り出す際、Index値が 0(ゼロ)始まりなので、-1 してる。

リトライする?

さぞや見事に当てられ、驚愕していることだろうと思う。
その様子を十分に堪能したら、そのカラクリを暴くべく再挑戦したくてそわそわしている方のために、リトライのチャンスを授けよう。
キャンバスにリトライボタンを追加。
fig39.png
当てられたショックを噛みしめる時間を待ってリトライボタンを登場させる。
fig40.png

リトライボタンの OnClickedイベントをつなぐ。
最後にスタート画面作成用のイベントを呼び出して完成。
fig41.png

できあがり

fig47.jpg
うさぎを選ぶ
fig49.jpg
4種のカード(0~3)自体は表示順がランダムだけど、カードの中身は数字の小さい順に並んでいるので、
こうやってスクショ撮って並べるとしくみがバレそう。
fig48.jpg

Hierarchy

最終的に使用しているUMGのパーツは以下
fig50.png
今回の手法として
テキストやボタンは書き換えずにすべて必要分を用意しておいて、表示/非表示を切り替えるしくみ

Variables

最終的に使用している変数
fig51.png
・・・カテゴリ的にVariablesにいるけど「変数」と呼ぶのはしっくりこないのは自分だけ?

Event/Function/Macro

命名に自信はないけど一応晒します
fig52.png

気づいたこと

今回最初にWidgetのImageを配列にして、32種のキャラ絵を仕込んでいます。
あとから4枚のカードを表示する際、「表示の遊び」として回転、大きさ、位置にゆらぎを入れるようにしているんだけど、
最初に作ったImage型の配列は、データ元というか いじらないImageとして使うつもりでした。
表示の直前、別に用意したImage型の配列に、必要な分だけをCopyして使ってやろうとしたわけです。
fig42.png
ところが、Copyしたと思っていたら、想定していたCopy(インスタンスが作られるみたいな)とは違っていて、結局表示用の一時的なImage型配列を使うのは諦めました。
途中 Set Render Transform Angle でリセットしているのはこのためです。
fig43.png
Getで取り出して、別の配列やImage型の変数に入れても、Canvas SlotのPosition、Render Transformに変更を加えると、しっかり大本のImageまで適用されていました。

試しに下のようなサンプルWidgetを作って表示してみました。
fig44.png
ImageOriginal は事前にキャンバスに配置済みのもの。
fig45.png
結果はというと、
fig46.png
詳しいことは調べ切れていないので、この件、本職の方にお譲りしたいのですが、
中途半端な知識で推察するに、どうやら Image型の変数は、「参照型」というやつなのでは?
という個人的見解に至っているところです。

最後に

なぜ当たるのか解りますか?
デジタルならではのランダム表現を入れたりしてアレンジしてるので、ちょっと解りにくいかもしれないけど、
仕組みの解明に挑んでみてはいかがでしょうか。随分とシンプルにできてるので、アレンジしやすいと思いますよ。

結構ご存じの方もいらっしゃるかなとは思いますが、
クリスマスにはネタバラシ編を追記しようと思っています。

作り終えてから、不安になってこのマジックの認知度をググってみたらそこそこ出てきた。
紙での作り方を紹介している良いサイトがありました。
ネタバレになりますが興味のある方はどうぞ → 日本数学検定協会

アドカレまだまだ続くよ~明日の記事をお楽しみに~
ハッピーホリデ~!

.
.
.

スペシャル付録 第一弾

確定したときのインパクトが弱いので、ついでに集中線を追加してみる。
まずマテリアルで丸を作る。
fig53.png
新たに専用のWidgetを作って、ユーザーWidgetとして組み込むことにする。

キャンバスに、最初から置かれているパネルの設定を少しいじる。
fig54.png
Is Variable を有効にするのと、Visibility を Collapse に変更しておく。不可視状態のスタート。

あとはWidgetブループリント
Construct Object from Class使って Image を量産するだけの関数を作る。
この方法は、詳細タブで設定できない分、いろいろ設定するノードが増えてしまうのが難点だけど、大量生産するときは仕方がないかな
ランダム要素も仕込めるのでそれなりに楽しい。
fig55.png
冒頭の Float型の変数 temp_Pivot はローカル変数。
変数にしているのは、Canvas Slot の Alignment と、Render Transform の Pivot の値を同じにしないと思ったように回転してくれないのが理由。
画面中央に配置するけど、基準のポジションをずらしているので ↓こういう状態になっているイメージ。
fig56.png

作ったImageは、後で利用するのですぐに配列に格納。

後半部分は、マテリアルをセットしてAngle(角度)をランダムにしてる。
fig57.png

この関数を、下準備としてEvent Pre Construct につないでコンパイルするとキャンバスがこうなってる。
fig59.png
コンパイルするたびに角度が変化するので面白い。
で、これを演出としてカスタムイベントで動くようにする。
fig60.png
fig61.png
準備するイベントと、ループするイベントの2つに分けてる。

Count 分動かしたら終了して再び非表示にするというイベント。
これでこの集中線Widgetは完成。

マジック用のWidgetにさっそく組み込んでみる。

まずキャンバスに集中線Widgetを配置。Palette の User Created の中に入っているので探してドラッグ&ドロップ。
fig62.png

fig63.png
すでに勢いが出てる。

あとはこの集中線Widgetを呼び出すところは、ここ。
fig64.png
確定して、リトライボタンを表示しようとする前。

線の数と更新回数はInt型の変数で変更可能。更新間隔は、Set Timer by Event で調整できる。

意外といい感じになります
動画を直接貼れないのでYoutubeへのアンカーをぺたり
https://youtu.be/XmmKcxnGBZ0
fig65.jpg

スペシャル付録 第二弾  ネタバラシ編

予告どおりこのマジックのタネについて追記します。

まず、10進数と2進数について
最初に選定する 1~15の数を2進数にしてみます。
fig66.png

そして、2進数の4つの桁を 桁ごとのグループとして扱います。
それぞれの桁で 1になっている数字を選別します。
fig67.png
ブループリントでカードを生成しているのがこれ
まずカードの番号0,1,2,3を計算してそれぞれを 1,2,4,8 にして生成用の関数を呼び出し
fig70.png
fig71.png
ForLoopで0~15を順番にビット演算 AND を使って判定しています。
受け取った引数 1,2,4,8 と AND を使うと、特定のビットの状態が確認できるので、
カード番号をに応じた4種類のカードが出来上がるというしくみです。
fig68.png

それぞれのカードを見せて、あれば 1 なければ 0 としてカウントします。
例えば11だったら、
fig69.png
あとはこの2進数を10進数に戻すだけ。
カード番号に対応する 1,2,4,8 のそれぞれが、あれば加算なければスルーということで
fig72.png
3番のカードが「ある」だったら 2³= 8 を加算。2番のカードが「ない」だったら加算しない。という感じで、
fig73.png

判定完了です。

今回 4枚のカードに 0~3 の数字をつけて管理しているのは、シャッフルすることで法則性を気付きにくくするのと、その番号を指数として使いたいからです。

fig74.gif

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2