久しぶりにiPhoneのゲームを作ってみようと思いまして、前から気になっていたPixiJSでやってみようと、vite + typescript で鼻歌交じりで作っていましたが、 「ちょっと実機で見てみよう、iOSのSafari(WKWebView)ってなんかコワイし」 と試してみると、やはりすくわれましたヨ、足元を。
3つのワナがありました。
- XCodeの参照の仕方が変更されている
- jsをmoduleで読み込めない
- PixiJSのAssetロードが絶対パスになる
XCodeの参照の仕方が変更されている
最近は調子に乗ってVSCodeで開発しているので(秀丸と半々くらいです)、 MacでもGithubを介してpullしたrepositoryをnpmでtypescriptをbuildしたdistをXcodeで参照すれば良いと思っていました。 (数年前までは唱えられなかった呪文です)
いざXCodeに成果物のdistフォルダを参照設定しようと昔のようにDropしても「参照」が選択肢に現れないんですよ。 projectそのものにDropすると「参照」の選択肢は出るのでそれでやってみてもダメなんですね。 WKWebViewではロードできません。どこがカレントなのかがわかりませんがとにかく読めません。
if let htmlURL = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "dist") {
let readAccessURL = htmlURL.deletingLastPathComponent() // → dist/
webView.loadFileURL(htmlURL, allowingReadAccessTo: readAccessURL)
WKWebViewを載せたViewControllerから相対的に(dist/index)使いたいので同階層にDropすると、 コピーしか道は残されていないのです。 htmlを更新するたびにコピーするのは面倒なので逆に「読み込めないでくれ」と思っていたら読み込めませんでした(ウラハラなキモチ)。
ChatGPTは黄色いフォルダではなく青いフォルダですよという昔のやり方を延々と繰り返します。 こうなるともう頼りにならないので、久々にやる気を出して開拓者モードに切り替わりました。 手には石斧でアンドレのようにワンショルダーのような気分です。
Wikiを見ていたら古舘伊知郎の例えが、ただの悪口も混じっていて面白かったです。
新日本プロレス参戦時は、実況アナウンサーである古舘伊知郎が、「大巨人」「巨大なる人間山脈」「一人民族大移動」などの表現を使ったことから、これらがアンドレのニックネームとなった(古舘はこの他にも「一人というには大きすぎる。二人といったら世界人口の辻褄が合わない」「人間というより化け物といった方がいいような」「都市型破壊怪獣ゴジラ」「怪物コンプレックス」「一人大恐竜」「ガリバーシンドローム」といった形容詞も使用している)。 wiki:アンドレ
長州力のアンドレの話も好きですが、一番好きなのは中島らもの
試合前にはウイスキー2本、缶ビール144本、ブランデー2本を空け、最後に5分間小便をした
という話が一番好きです。
ほどなくしてBuild Phasesの「Copy Bundle Resources」でdistフォルダを指定することにたどり着きました。
これでPixiJSのコードを更新し、npm run buildするだけで、iOSアプリ側も最新のソースを読み込んでくれるようになります。
jsをmoduleで読み込めない
しかしこれで許してくれるほどAppleは甘くありません。
index.htmlを読み込めはしますが動きません。
さらに石斧で開拓していくとtype="module" crossoriginがダメで、
jsの読み込み完了を待つ必要があることにたどり着きました。
<script type="module" crossorigin src="/assets/index-B2RAAgUu.js"></script>
// ↑これを↓こうする必要がある
<script defer src="/assets/index-B2RAAgUu.js"></script>
まあいちいちbuildするたびに書き換えるのはお猿さんなのでスクリプトで対応します。
// replace-defer.js
import fs from "fs/promises";
import path from "path";
async function addDeferToScript() {
const distDir = path.resolve("./dist");
const indexPath = path.join(distDir, "index.html");
try {
let html = await fs.readFile(indexPath, "utf8");
// type="module" crossorigin を defer に置換
html = html.replace(
/type="module" crossorigin/,
'defer'
);
await fs.writeFile(indexPath, html, "utf8");
console.log("defer属性を付与しました");
} catch (e) {
console.error("ファイル読み書きエラー:", e);
}
}
addDeferToScript();
これをpackage.jsonのbuildで呼び出します。
"build": "npm run lint && tsc && vite build && node replace-defer.js",
PixiJSのAssetロードが絶対パスになる
普通にWKWebViewに乗せたPixiJSでは画像が読み込めません。画像をどこに置いても読めません。
いろいろ調べましたが、PixiJSでは画像を絶対パスで扱うようです。
WKWebViewはローカルファイル(file://)を絶対パスで読み込むことができません。
藤田晋に言わせると「解決しがたい問題」と答えることでしょう。
リモートファイル(https://)での絶対パスでは画像を読み込めますが、アプリを使用する際に常にネットから画像を引っ張ってくるのはよろしくありません。
仕方がないので画像を文字情報にして読み込むことにしました。
base64化です。スクリプトで変換します。
// encode-assets.js
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dir = path.join(__dirname, "public/assets");
const outFile = path.join(__dirname, "src/assets.ts");
const exts = [".png", ".jpg", ".jpeg", ".gif"];
const assets = [];
for (const file of fs.readdirSync(dir)) {
const ext = path.extname(file);
if (!exts.includes(ext)) continue;
const name = path.basename(file, ext);
const buffer = fs.readFileSync(path.join(dir, file));
const base64 = buffer.toString("base64");
const mime = `image/${ext.replace(".", "")}`;
assets.push(` { alias: "${name}", src: "data:${mime};base64,${base64}" }`);
}
const code = `
// 自動生成ファイル(編集しないでください)
import { Assets } from "pixi.js";
export const assetList = [
${assets.join(",\n")}
];
export async function loadAssets() {
for (const asset of assetList) {
Assets.add(asset);
}
await Assets.load(assetList.map(a => a.alias));
}
`;
fs.writeFileSync(outFile, code);
console.log(`✅ ${outFile} を生成しました`);
これもやはりbuild時に実行しますのでdefer書き換えとともに以下のようになりました。
"start": "node encode-assets.js && npm run dev",
"build": "node encode-assets.js && npm run lint && tsc && vite build && node replace-defer.js",
使用時には以下のようにします。
import { loadAssets } from "../assets";
await loadAssets();
// 拡張子をはぶいたエイリアスで呼び出せます
const sprite = new Sprite(Texture.from("human"));
おわりに
今回もAppleにしてやられましたが、Appleのほうは当然ながらコチラのことは何も気にしていないのでしょう。
2度目の転生を果たしたガニシュカ大帝のごとく小虫程度にしか…
しかし我々は、いつでもプラットフォームを選択することはできます!(涙)