結論:動く。ただし「動く」と「ゲームになる」の間には深い谷がある
昔、Excelでマリオのステージ1が遊べるやつが流行りましたよね。あれを見たときの「業務ツールをそこまで逸脱させるのか」という感動が、ずっと頭の片隅にありました。
で、ふと思ったわけです。**Qlikでもできるんじゃないか、と。**
QlikはBIツールです。シートは静的で、基本的に動きません。オブジェクトはデータを可視化するためのもので、キャラを動かすためのものじゃない。……でも、それを言われると逆にやりたくなる。というわけで今回は、Qlik Cloudの標準機能だけを縛りプレイで使って、パックマン風の迷路をどこまで動かせるか試してみました。
拡張機能(nebula.js)は一切使いません。それは後編でやります。前編は「素のQlikでどこまでいけて、どこで壁にぶつかるか」の記録です。ぶつかった壁が、そのまま後編への理由になります。
ステップ1:盤面をデータとして持つ
迷路を「絵」ではなく「データ」として持ちます。1=壁、0=通路。とりあえず、20×20で。
Maze:
LOAD * INLINE [
Y, X, Cell
0, 0, 1
0, 1, 1
...
];
Yが行、Xが列、Cellがそのマスの状態。これをロードすると81行のテーブルができます。ここまではただのデータロード。何の変哲もない。
ステップ2:ピボットテーブルでグリッドにする(最初の小さな壁)
さて、このデータを盤面の形に並べたい。最初、素直にストレートテーブルを置きました。
縦に81行並ぶだけでした。
そりゃそうです。ストレートテーブルには「X方向(横)に展開する」という概念がない。9×9のマス目にはならない。
正解はピボットテーブルでした。
– 行に Y
– 列に X
– 値に =Sum(Cell)
こうすると Y が縦・X が横に展開されて、9×9のグリッドに `0` と `1` が並びます。盤面の土台完成。地味だけど、ここで「テーブルを画面に見立てる」という今回の方針が固まります。
ステップ3:数字を色に変えてドット絵にする
0 と 1 が並んでるだけだと迷路に見えない。セルの背景色を数式で塗り分けます。値のメジャーの背景色を「数式で指定」にして:
if(Sum(Cell)=1, rgb(33,33,99), rgb(0,0,0))
壁=紺、通路=黒。これで一気に盤面っぽくなります。
ただし数字(0と1)が見えたまま。これを消す小技として、文字色にも背景と同じ数式を入れます。背景と文字が同色になって、数字が視覚的に消える。色の面だけが残る。
この瞬間、テーブルが「迷路の盤面」に見えます。ちょっと感動する。
ステップ4:テーブル感を消す(沼①:シートCSSの制約)
とはいえ、まだ「表」の匂いが残っています。X・Yのラベル、セルの区切り線、そしてセルが正方形じゃなくて横長。ここを潰して「画面」にしたい。
まずネイティブのスタイリング設定で、消せるものは消しました。ヘッダーや区切り線、合計行など、プロパティパネルでかなり潰せます。
問題はここから。セルを正方形に固定したい。 これはネイティブでは届かないので、CSSの出番です。
Qlik Cloudは2025年11月から、シート単位でカスタムCSSを入れられるようになりました。シートのスタイリングプロパティに「カスタムスタイル」のトグルがあります。……が、ここに大きな落とし穴がありました。
要素セレクタでこう書いたら:
.qv-pivot-table td {
padding: 0;
}
「`padding`プロパティは許可されていません」というエラー。
Qlik CloudのシートCSSは、入力されたCSSをサニタイズします。アプリ機能を壊す/隠す要素は無視される。そして標準要素セレクタ(tdなど)に対して効かせられるプロパティが制限されている。paddingすら通らない。display:none に至っては、ユーザー定義のクラス/要素にしか効きません。
つまり「CSSで全部消して完全に画面化」という当初の目論見は、Qlik Cloud側の設計で最初から封じられていたわけです。公式も「CSSは最終手段。ネイティブ設定を優先せよ。CSS構造はAPIではなく予告なく変わる」とはっきり言っています。
ここが前編の壁その1。素のQlikでは、見た目を完全に作り込むことはできない。 セルが横長につぶれているのは、直せませんでした。正直に「できなかった」と書いておきます。
ステップ5:キャラを動かす
見た目はここで妥協して、いよいよ「動き」です。ここが前編のヤマ場。
やることは3つ:
1. キャラの座標を変数で持つ(vX, vY、初期値 1,1)
2. 移動ボタンでその変数を書き換える
3. 色数式に「キャラの位置は黄色」を追加
色数式にキャラを足すとこうなります(キャラ>壁>通路の優先順):
if(Max(X)=num($(vX)) and Max(Y)=num($(vY)), rgb(255,221,0),
if(Sum(Cell)=1, rgb(33,80,220), rgb(0,0,0)))
Max(X)/Max(Y) を使っているのは、ピボットのセル内は集計値になるため。1セル=1行なので Max でその座標が正しく取れます。あと num() で変数を囲っているのは後述の事故対策(最初は付けてませんでした)。
移動ボタンは、Buttonオブジェクトを置いて「アクションを追加」→「変数値の設定」。たとえば下ボタンは:
– 変数 vY
– 値 =$(vY)+1
先頭の= が超重要(後述の事故の伏線)。あと「実行時評価」はオンにしておきます。複数アクションを連鎖させたとき、前のアクションの結果を次が受け取れるようにするためです。
ボタンを押すと、黄色いキャラがマス目を移動します。BIツールの上をキャラが歩いている。 これが見たかった。
ステップ6:壁判定でやらかす(沼②:アプリが完全にフリーズ)
このままだと壁をすり抜けて盤面の外にも出られてしまう。壁で止める判定を入れます。
「移動先のセルが壁(Cell=1)なら動かない、通路なら進む」——これをボタンの値の数式に持たせればいい。Set分析で移動先のCell値を取って、if で分岐。
で、やらかしました。
最初、値の数式にこう書いたんです(先頭の= を付け忘れて):
if(Sum({<X={$(vX)}, Y={$(vY)+1}>} Cell)=1, $(vY), $(vY)+1)
「変数値の設定」の値は、先頭に =が無いと数式ではなくただの文字列として変数に入ります。つまり vY に、この長い if 文が文字列そのまま流し込まれた。
すると色数式側の $(vY) がその文字列を丸ごと展開し、81セルそれぞれで重いSet分析を評価しようとし、さらにその中で vY を参照し……アプリが完全にフリーズしました。 ロード中がぐるぐる回ったまま固まる。
しかもタチが悪いのが、閲覧モードで変数を直そうとしても、触るそばから再計算が走ってまた固まる。データロードエディタを開いても、短いスクリプトなのにロードが終わらない。
ここで一瞬、血の気が引きます。「本番テナントで何かやらかしたのでは」と。
でも大丈夫でした。この惨状は新規アプリのセッション1個の中に完全に閉じている。他のアプリにも本番アプリにも、一切波及していません。テナントは何ともない。固まっているのは「このアプリを開いている自分のブラウザセッション」だけ。
ステップ7:復旧
復旧の手順はこうでした:
1. ブラウザのタブを閉じて開き直す(詰まったセッションをリセット)
2. 開き直したらシートを見ずにデータロードエディタへ直行(盤面を表示するとまた固まるので)
3. スクリプト末尾に安全弁を追加してリロード:
SET vX = 1;
SET vY = 1;
これでリロードのたびに変数が数値へ初期化される。アプリが息を吹き返し、キャラもスタート地点に戻りました。
この SET の2行は、以降ずっと安全弁として残しました。何かやらかしても、リロードすれば必ず初期状態に戻せる。精神衛生上とても良い。
ステップ8:壁判定リベンジ
事故の原因は「変数に式そのものを書き込もうとした」ことと「= の付け忘れ」。設計を見直して、変数には数値しか入れない・式は必ず数値に潰れる形にする方針で書き直しました。
まず色数式を num() で防御(変数が変な値でも数値化しようとして、重い再帰評価に化けにくくする)。その上で、下ボタンの値をこう:
=if(Sum({1<X={"$(vX)"}, Y={"$(=vY+1)"}>} Cell)=1, vY, vY+1)
ポイント:
– 先頭 = は必須(もう忘れない)
– {1<…>} の先頭 1 で全レコードを母集合に明示(選択状態に影響されない)
– Set分析の値を “…” で文字列化して座標を渡す
– $(=vY+1) の内側 $(=…) で「移動先座標」を先に数値計算してから渡す
– 返り値は vY / vY+1 と素の数値($() の入れ子をやめたのが固まり防止の肝)
1回だけ慎重に押す。……動いた。しかも壁の手前でピタッと止まる。 リベンジ成功です。あとは同じパターンを上下左右の4方向に横展開。
ステップ9:一応「ゲーム」にする
歩けるだけではゲームと呼べない。目的が要る。標準機能の範囲で乗せられるものを足しました。
ゴール:特定マス(右下)を緑で描画し、色数式に1行足すだけ。
手数カウンター:変数 vMoves を作り、各ボタンに「=vMoves+1」のアクションを追加。押すたびに手数が増える。
クリア判定:テキストオブジェクトで、ゴール到達時に表示を切り替え:
=if($(vX)=19 and $(vY)=19,
'CLEAR! ' & $(vMoves) & '手',
'現在地 (' & $(vX) & ',' & $(vY) & ') 手数: ' & $(vMoves))
リセットボタン:vX/vY を 1 に、vMoves を 0 に戻す。
これで「少ない手数でゴールを目指す」という遊びが成立します。一応、ゲームです。
それで、これはゲームなのか?
正直、まだ「ゲーム」と呼ぶには足りません。
作れたのは「ボタンで動かせる、クリア判定つきの本格迷路」。でも、パックマンの心臓部——ドットを食べて消す、敵が自動で追ってくる——これは標準機能では作れませんでした。
理由は突き詰めると2つ。
1. Qlikにはゲームエンジンのようなゲームループはない。ただし、Dashboard bundle のアニメーターで連番ディメンションを一定間隔で切り替えれば、チャートはフレームごとに再計算されます。つまり「時間」はないけれど、「時間っぽいもの」は作れます。ただし、本当に状態を更新しているわけではなく、毎フレームをゼロから計算し直しているだけ。そのため、敵を動かすことはできても、本格的なゲームの状態管理には限界があります。
2. 安全な状態管理ができない。ドットを食べて消す=状態を書き換えて保持する、を変数でやろうとすると、まさにステップ6のフリーズ地獄に片足を突っ込む。
つまり「ゲームにならない」のは手を抜いたからではなく、BIツールの構造的な限界に正面からぶつかっているということ。ここが縛りプレイの終着点です。
後編へ
というわけで、素のQlikの限界がはっきり見えました。時間も、安全な状態管理も、思い通りの描画も足りない。
これを全部ぶち抜く方法が1つあります。nebula.jsで独自の拡張機能を作り、canvasを召喚すること。canvasなら本物のゲームループが回せるし、描画も自由。しかもスコアやステージ構成を裏のQlikデータモデルから供給すれば、「ただのゲーム移植」ではなく「データ駆動のゲーム」としてBIに帰着させられる。
後編では、本番テナントを1バイトも汚さずに(ローカルの nebula serve を使って)、この迷路を本気で動かします。お楽しみに。
週末の工作にちょうどいい題材でした。事故って半分血の気が引いたのも含めて。


