はじめに
子どもの頃、父親の実家がある広島へ、東京から車や新幹線で帰省していました。
東京から広島までは長距離です。特に子どもの頃は移動時間が異常に長く感じられ、とにかく暇だった記憶があります。
そんな移動中、車窓から流れていく景色を眺めながら、よく頭の中で遊んでいました。
窓の外を、鉛筆で描いたような棒人間が走っている。
建物が現れたらジャンプする。突然敵が現れて戦い始める。飛び越えたり、攻撃したり、時には吹き飛ばされたり。
もちろん、実際に棒人間が見えていたわけではありません。すべて頭の中の妄想です。
最近、ふとそんな子どもの頃の遊びを思い出しました。
そして、こんなことを考えました。
「今の技術なら、あの頃の妄想を実際に再現できるのでは?」
せっかくなので生成AIにも手伝ってもらい、Webアプリとして作ってみることにしました。
最終的には、スマートフォンのカメラ映像に棒人間を重ね、Google CloudのCloud Runで公開するところまで試しています。
今回は、その制作過程と技術構成を紹介します。
1. 作りたいもの
実現したかったのは、車窓の景色を背景に、棒人間が自由に動き回るアニメーションです。
イメージは次のとおりです。
- 風景が横方向に流れ、棒人間が走り続ける
- 突然ジャンプしたり、宙返りしたりする
- 敵の棒人間が出現して戦闘する
- 必殺技や連続攻撃を繰り出す
- 戦闘が終わると、再び何事もなかったように走り始める
特に重視したのは、何も操作しなくても棒人間が自動でアクションを繰り出すことです。
子どもの頃の妄想も、決まったシナリオで動いていたわけではありません。
次に何をするか分からない方が、当時の感覚に近いと考えました。
完成した試作品
※実際はイラスト版は右から左へスクロールし続けます
今回制作した試作品では、次の機能を実装しています。
| 機能 | 内容 |
|---|---|
| 棒人間の描画 | 鉛筆で描いたようなシンプルな線画 |
| アニメーション | 走る・ジャンプ・攻撃など |
| 自動再生 | ランダムなアクション |
| 背景表示 | イラスト風景・カメラ映像・ローカル動画 |
| アクション操作 | ボタンやキーボードで指定 |
| 速度変更 | スライダーで調整 |
| 全画面表示 | 対応ブラウザで利用可能 |
| クラウド公開 | Cloud RunによるWeb配信 |
2. 技術選定
最初はUnityやBlenderなども候補として考えました。
ただ、今回は本格的な3Dゲームや映像制作が目的ではありません。
ブラウザ上で棒人間を動かし、最終的にはスマートフォンのカメラ映像に重ねたいだけです。
そこで、できるだけシンプルな構成を選択しました。
| 技術 | 用途 |
|---|---|
| HTML / CSS | 画面と操作パネル |
| JavaScript | アニメーション制御 |
| Canvas API | 棒人間と背景の描画 |
| MediaDevices API | カメラ映像の取得 |
| Nginx | HTMLの配信 |
| Docker | コンテナ化 |
| Google Cloud Run | Webアプリのホスティング |
| ChatGPT | コード生成と改善の支援 |
今回はReactなどのフレームワークも使用していません。
HTMLファイル1つでアニメーションとカメラ機能を実現できました。
また、周囲の物体を認識する必要はないため、OpenCVやWebXRなどの技術も採用していません。
3. HTMLとCanvasで棒人間を動かす
最初に作成したのは、シンプルな棒人間が走るアニメーションです。
棒人間は、頭・胴体・左右の腕・左右の脚という構造です。
Canvas APIを使用すると、円と線でキャラクターを描画できます。
棒人間の描画
以下は仕組みを説明するための簡略化したコードです。
function drawStickman(ctx, x, y) {
ctx.beginPath();
// 頭
ctx.arc(x, y - 60, 12, 0, Math.PI * 2);
// 胴体
ctx.moveTo(x, y - 48);
ctx.lineTo(x, y - 20);
// 腕
ctx.moveTo(x, y - 42);
ctx.lineTo(x - 20, y - 30);
ctx.moveTo(x, y - 42);
ctx.lineTo(x + 20, y - 30);
// 脚
ctx.moveTo(x, y - 20);
ctx.lineTo(x - 15, y);
ctx.moveTo(x, y - 20);
ctx.lineTo(x + 15, y);
ctx.stroke();
}
実際のアプリでは、腕や脚の座標を時間に応じて変更することで、走る動きを表現しています。
例えば、Math.sin() を使って腕と脚を周期的に動かせば、ランニングアニメーションになります。
const phase = elapsed * speed;
const movement = Math.sin(phase);
さらに、腕と脚を逆方向に動かしたり、体の傾きを変えたりすることで、より自然な動作に近づけました。
ジャンプを実装する
次に追加したのがジャンプです。
ジャンプは放物線を利用した簡単な計算で表現しています。
実装の考え方は次のとおりです。
const p = actionTime / actionLength;
// p は 0〜1
const jumpHeight = 180 * 4 * p * (1 - p);
const y = ground - jumpHeight;
ジャンプ開始時と終了時には高さが0になり、中間で最大高度になります。
これにより、棒人間が自然にジャンプして着地する動きを再現できます。
厳密な物理シミュレーションではありませんが、今回のアニメーションには十分でした。
4. アクションを17種類に拡張
最初は走る、ジャンプする、パンチするといった単純な動作だけでした。
ただ、これでは長時間眺めていると単調になります。
そこでChatGPTと相談しながら、アクションを増やしていきました。
最終的には、通常の走行に加えて17種類のアクションを実装しています。
| 移動・ジャンプ | 戦闘・その他 |
|---|---|
| ジャンプ | パンチ |
| 連続ジャンプ | ハイキック |
| 大ジャンプ | 回し蹴り |
| 宙返り | 連続攻撃 |
| ダッシュ | 棒人間バトル |
| スライディング | チャンバラ |
| 前転 | つまずく |
| 回避 | 手を振る |
| ハードル越え |
途中のバージョンでは空を飛び続けるアクションも作りました。
しかし、実際に動かしてみると、子どもの頃にイメージしていた「地面を走ってジャンプする棒人間」とは少し違いました。
そのため、飛行アクションは削除しています。
代わりに、スライディングや前転、回避、戦闘などの動きを充実させました。
自動再生の工夫
ランダムにアクションを選択するだけでは、同じ動きが連続することがあります。
そこで、直近2回のアクションを候補から除外する仕組みを追加しました。
function randomAction() {
const pool = actionKeys.filter(
k => k !== lastAction &&
k !== previousAction
);
return pool[
Math.floor(Math.random() * pool.length)
];
}
アクション終了後には再び走る状態に戻し、次の動作まで少し間を空けています。
これだけでも、単純なランダム再生より自然な流れになりました。
5. スマートフォンのカメラ映像に重ねる
ここからが、今回の制作で特に試したかった機能です。
最初はイラスト風景を背景にしていましたが、せっかくなら実際の車窓を背景にしたいと考えました。
そこで、スマートフォンのカメラ映像に棒人間を重ねることにしました。
仕組みは意外とシンプル
画面を2つのレイヤーに分けています。
┌─────────────────────────┐
│ Canvas │
│ 棒人間のアニメーション │
│ │
├─────────────────────────┤
│ video │
│ カメラのリアルタイム映像 │
└─────────────────────────┘
実際には2つの要素を同じ表示領域に重ねます。
HTMLは以下のような構成です。
<section class="stage">
<video
id="backgroundVideo"
autoplay
muted
playsinline>
</video>
<canvas id="scene"></canvas>
</section>
CSSでvideoとcanvasを重ねます。
.stage {
position: relative;
}
.stage video,
.stage canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.stage video {
object-fit: cover;
z-index: 1;
}
.stage canvas {
z-index: 2;
pointer-events: none;
}
カメラ映像を下のレイヤーに表示し、透明なCanvasに棒人間を描画します。
そのため、本格的なARエンジンを使わなくても、ARのような見た目を実現できます。
ただし、現実空間への位置固定や、物体の前後関係を判断する機能はありません。
今回は、あくまでAR風の映像合成です。
カメラ映像を取得する
カメラの取得には navigator.mediaDevices.getUserMedia() を使用します。
以下は実装の主要部分を抜き出したコードです。
const stream =
await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
facingMode: { ideal: "environment" },
width: { ideal: 1280 },
height: { ideal: 720 }
}
});
const video =
document.getElementById("backgroundVideo");
video.srcObject = stream;
await video.play();
facingMode: "environment" は背面カメラを希望する設定です。端末によって実際に使用されるカメラは異なります。
カメラを停止する際には、MediaStreamのトラックも停止します。
stream.getTracks().forEach(track => track.stop());
カメラ映像をクラウドに送信しない
今回の構成で重要なのは、カメラ映像をCloud Runに送信していないことです。
カメラの取得、表示、棒人間との合成は、すべてブラウザ内で実施します。
動画ファイルを読み込む機能も、URL.createObjectURL() で端末内の動画を表示する方式です。
サーバー側で動画を解析する処理はありません。
また、カメラAPIの利用には、ユーザーの許可と安全な接続が必要です。
ローカルの localhost やCloud RunのHTTPS URLであれば、安全なコンテキストの条件を満たせます。
6. ローカル環境で動作確認
今回はMacBookを使用して開発しました。
HTMLファイルを用意した後、Pythonの簡易HTTPサーバーで動作確認しています。
python3 -m http.server 8000
次のURLにアクセスします。
http://localhost:8000/window-stickman-ar-v3.html
ブラウザで棒人間のアニメーションを表示できました。
その後、スマートフォンの実機カメラでも動作を確認しています。
今回のような小規模なHTMLアプリであれば、複雑な開発環境を作らなくても検証を始められるのは便利です。
7. Google Cloud Runにデプロイする
ローカル環境で動作することを確認できたので、次はCloud Runに載せてみました。
今回の目的は、スマートフォンからURLを開くだけで棒人間を動かせるようにすることです。
Cloud Runを選んだ理由には、普段からGoogle Cloudに関心があり、実際に使ってみたかったこともあります。
なお、静的HTMLを配信するだけなら、Firebase Hostingなども選択肢になります。
必ずしもCloud Runである必要はありませんが、コンテナ化やデプロイを試す題材として採用しました。
今回のアーキテクチャ
MacBook
│
│ HTML / Dockerfile
▼
Cloud Build
│
│ コンテナイメージ作成
▼
Artifact Registry
│
▼
Cloud Run
│
│ HTTPSでHTML配信
▼
スマートフォンのブラウザ
├── カメラ映像取得
├── Canvas描画
└── 棒人間のアニメーション
Cloud Run側ではHTMLを配信するだけなので、バックエンドのAPIなどは実装していません。
ディレクトリ構成
次のような構成にしました。
window-stickman-cloud-run/
├── index.html
├── Dockerfile
├── .dockerignore
└── README.md
ローカルで検証したHTMLを index.html として配置しています。
Dockerfileを作成する
今回はNginxの非rootイメージを使いました。
FROM nginx/nginx-unprivileged:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 8080
これだけです。
nginx/nginx-unprivileged は、一般ユーザー権限でNginxを実行するためのイメージです。
デフォルトのリッスンポートが8080なので、Cloud Runのポート設定とも合わせやすい構成です。
参考:NGINX Unprivileged Docker Image
Google Cloudの準備
Google Cloud CLIをインストールしたうえで、初期設定を行います。
gcloud init
プロジェクトを設定します。
PROJECT_ID="your-project-id"
gcloud config set project "$PROJECT_ID"
続いて、必要なAPIを有効にします。
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
Google Cloudのプロジェクトには課金アカウントが紐づいている必要があります。また、デプロイするアカウントやビルド用サービスアカウントに必要なIAM権限も求められます。
Cloud Runへデプロイ
Dockerfileのあるディレクトリから、以下を実行します。
gcloud run deploy window-stickman \
--source . \
--region asia-northeast1 \
--port 8080 \
--memory 256Mi \
--cpu 1 \
--max-instances 2 \
--no-invoker-iam-check
主な設定は以下のとおりです。
| 項目 | 設定 |
|---|---|
| サービス名 | window-stickman |
| リージョン | asia-northeast1(東京) |
| ポート | 8080 |
| メモリ | 256MiB |
| CPU | 1 vCPU |
| 最大インスタンス | 2 |
| アクセス | 未認証アクセスを許可 |
--source . を指定すると、Cloud Buildを利用してソースコードからコンテナイメージを作成し、そのままCloud Runへデプロイできます。
Dockerfileがあれば、その内容がビルドに使用されます。
そのため、今回はMacにDocker Desktopをインストールしなくてもデプロイできました。
なお、--no-invoker-iam-check は一般公開を許可する設定です。認証なしで誰でもアクセスできる状態になるため、実際に使用する場合は公開範囲を理解したうえで指定してください。
デプロイ後のURLを確認する
以下のコマンドでURLを確認できます。
gcloud run services describe window-stickman \
--region asia-northeast1 \
--format='value(status.url)'
Cloud Runが発行するHTTPS URLを使って、ブラウザからアクセスします。
今回のお試しでは、Cloud Runへのデプロイまで進めることができました。
8. 費用と本格公開に向けた注意点
今回はあくまで個人開発のお試しです。
小規模な静的HTMLアプリなので、Cloud Runの無料枠に収まる可能性はあります。
ただし、必ず無料になるわけではありません。
Cloud BuildやArtifact Registryには別の料金体系があり、通信量や利用状況によっても費用が発生します。
また、最大インスタンス数を制限しても、費用の上限が保証されるわけではありません。
参考:Cloud Runの料金
本格的に公開するなら
今後、不特定多数の人に利用してもらう場合には、少なくとも次の項目を追加で検討したいと考えています。
| 項目 | 今後の対応 |
|---|---|
| IAM・公開範囲 | 未認証アクセスを許可するか再検討 |
| コスト管理 | 予算アラート、課金状況の確認 |
| セキュリティ | コンテナ更新、HTTPセキュリティヘッダー |
| CI/CD | GitHub Actionsによる自動デプロイ |
| 運用監視 | Cloud Logging、エラー監視 |
| 独自ドメイン | 必要に応じて設定 |
| パフォーマンス | 実機別の描画負荷や消費電力の確認 |
| UI/UX | スマートフォンの操作性改善 |
特にコスト管理は重要です。通常の予算アラートは通知する仕組みであり、支出を自動的に止めるものではありません。
今回は動作検証を目的としているため、これらの本格運用向けの設定までは実施していません。
本番公開を行う場合は、別途設計・検証する必要があります。
9. 実際に作ってみて
今回、意外だったのは、子どもの頃に頭の中で動かしていた棒人間を、HTMLとJavaScriptだけでかなり再現できたことです。
最初は「棒人間を走らせる」だけのシンプルなものを想像していました。
ところが、実際に動かし始めると、ジャンプや戦闘などの動作も増やしたくなりました。
生成AIと相談しながら少しずつアクションを追加し、ブラウザで確認し、気になるところを修正する。
そんな開発を繰り返しているうちに、気がつけば17種類ものアクションを持つ棒人間になっていました。
生成AIを使うことで、アニメーションを一からすべて手作業で実装するよりも、試行錯誤を進めやすくなったと感じています。
一方で、生成されたコードが意図した動作をするかは、実際のブラウザで確認する必要があります。今回も、作って動かし、イメージと違う部分を修正する作業を繰り返しました。
今後やってみたいこと
今回は車窓の風景を認識する機能は実装していません。
そのため、棒人間が実際の電柱や建物に反応するわけではありません。
将来的には、画像認識によって障害物に合わせてジャンプする仕組みも面白そうです。
また、シーンごとのアクションを自然につなげたり、もっと複雑な戦闘を自動生成したりすることも考えています。
ただ、今の段階でも、スマートフォンを車窓に向けることで、子どもの頃に想像していた光景にかなり近づけられました。
まとめ
今回試した内容をまとめます。
- HTMLとCanvas APIで棒人間アニメーションを実装
- ジャンプや戦闘など17種類のアクションを追加
- JavaScriptでランダムな自動再生を実装
- MediaDevices APIでカメラ映像に棒人間を合成
- Nginxコンテナを作成してCloud Runへデプロイ
技術的には、それほど複雑な構成ではありません。
しかし、昔の自分が想像していたものを、実際に動く形にできたのは面白い体験でした。
今後も、生成AIとGoogle Cloudを使いながら、このような個人開発を続けてみたいと思います。
当時の自分にこのアプリを見せたら、東京から広島までの長い移動時間も、きっと退屈しなかったでしょう。
参考資料
- MDN - Canvas API
- MDN - MediaDevices.getUserMedia()
- Google Cloud - Cloud Run
- Google Cloud - ソースコードからのデプロイ
- Google Cloud - Cloud Runの公開アクセス
- Google Cloud - Cloud Runの料金
※本記事は2026年10月時点の試作内容に基づいています。クラウドサービスの仕様、料金、必要な権限などは変更される可能性があります。
