Open10
悪質な月額制Webサイトを模したゲームを作る

作るもの
-
退会手続きがめんどくさい悪質な月額課金制サイトを模したスマホゲームアプリ
- 悪質な月額課金制サイトの、意図的に退会手続きを分かりにくくしたり、手間を増やしたりする特徴(以下例)をゲームにする
- 退会フォームが見つかりにくい場所にある
- ログインしないと退会できないが、ログイン情報を忘れさせるような設計
- 問い合わせフォームしかなく、明確な退会方法が書かれていない
- 電話やFAXなど、わざと面倒な手段しか用意していない
- 「退会=アカウント停止」であって、個人情報は消されない仕様
- 悪質な月額課金制サイトの、意図的に退会手続きを分かりにくくしたり、手間を増やしたりする特徴(以下例)をゲームにする
目標
- GooglePlayストアに公開して、広告収入で収益化してみる
- 月に500円稼げれば、自分が悪質サイトに払ってる分を相殺できる

開発〜GooglePlayストア公開までの流れ
- 開発
- Google AdMobに登録
- アプリに広告コードを組み込む
- アプリのビルド
- Google Play Consoleに登録($25)
- 審査
- 公開

開発環境構築
Android Studio
- 以下サイトからインストーラーをダウンロードし、インストーラー実行(ウィザードはすべてデフォルト値でインストール)
- 実行して、初回起動でSDKなどをダウンロード&インストール
- とりあえず標準インストール
Ionicプロジェクト
-
Ionicプロジェクトを作成する
npm install -g @ionic/cli ionic start {プロジェクト名} blank --type=react
- 途中でIconicアカウントの作成をするかどうか聞かれる
- とりあえずアカウント不要
Ionicとは
- クロスプラットフォームモバイルアプリを構築するためのオープンソースの集まり
- Ionic Framework(モバイルUI特化のUIコンポネントフレームワーク)
- ionic/react, ionic/angular, ionic/vue
- セットアップが簡単になる
- 純粋にviteでreactプロジェクト作成して、モバイルアプリ作成のパッケージ(capacitor等)揃えるより楽だった
- 途中でIconicアカウントの作成をするかどうか聞かれる
-
Reactアプリを静的ファイルにビルドする
ionic build
-
Androidプラットフォームの追加
ionic cap add android
-
Android Studioで開く
ionic cap open android

ソース変更->ビルド->確認する
- ソース変更(追加/修正)
- ブラウザで確認
iconic serve
- ビルドもしくは
ionic build
npm run build
- Capacitorに反映もしくは
npx cap copy android
npx cap copy android
- Android Studioを開く
npx cap open android
- Android Studio上で、エミュレータ起動▶️

IonicとCapacitor
Ionic
- モバイルUI特化のフレームワーク
- React, Vue, Angular対応
- プロジェクト作成に使えるcliツール
- モバイルアプリを作るために必須というわけではないが、モバイルアプリ作成が始めての場合は、これでプロジェクト作成した方が楽かも
Capacitor
- Webアプリをネイティブアプリとして動かすラッパー技術
- Webアプリからデバイス機能(位置情報、カメラなど)にアクセスできるようにする仕組み
- Reactなどで作られたWebアプリをこのCapacitorを通すことで、Android Studioに持っていき、エミュレータなどで確認可能

ReactのStoreについて
概念
- アプリケーション全体で管理される状態管理機能
- 実際には、Redux、Zustand、Recoilというパッケージが提供している機能
- アプリの規模などでどれを使うかを検討する
メリット
- 状態管理を一元化することで、保守性が向上する
- 認知負荷が減る
- コンポーネント間で、状態の共有が容易になる
- Store使わない場合は、props、Contextを使うが、規模が大きくなると複雑性が増す
- UIとロジック(状態管理)を分離することで、変更の影響範囲が局所化される
- 見た目の変更は、動作には影響させない
Zustandでの使い方
- zustandのインストール
npm install zustand
- Storeを定義する
// store.ts import { create } from 'zustand'; // ✅ 状態の型定義:状態(count)と、それを操作する関数(increment, decrement)を定義 type State = { count: number; increment: () => void; decrement: () => void; }; // ✅ storeの作成:状態の初期値と、それを操作する関数の実装 export const useCounterStore = create<State>((set) => ({ count: 0, // 初期状態 increment: () => set((state) => ({ count: state.count + 1 })), // 状態の加算処理 decrement: () => set((state) => ({ count: state.count - 1 })), // 状態の減算処理 }));
- コンポーネントで使用
// Counter.tsx import { useCounterStore } from './store'; export function Counter() { const count = useCounterStore((s) => s.count); //セレクタ(s)を使用して取り出す const increment = useCounterStore((s) => s.increment); const decrement = useCounterStore((s) => s.decrement); return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>−</button> </div> ); }
- ストアから変数や関数を取り出す際は、セレクタ(s)を使用して取り出す方が良い。
- セレクタを使用しない場合、ストアの変数、関数を全て取得して来るので、不必要な状態依存 -> レンダリングが発生してしまう可能性あり

Google Ads(広告)挿入について
前提
- Google Adsとは、Googleが提供するオンライン広告サービス
- モバイルアプリに広告を挿入する際は、AdMobというプラットフォームを使う
- Webサイトに入れる場合は、AdSense
- AdMobなどで挿入できる広告には、さまざまな種類があるが、主要な広告は以下の3つ
- バナー広告
- インタースティシャル広告
- リワード広告
広告の作り方から収益情報管理まで
- AdMobにGoogleアカウントでログイン
- AdMob内で、アプリを作成(アプリID発行)
- アプリ内に、広告ユニット(バナー、インタースティシャル、リワード、etc...)を作成(ユニットID発行)
- そのユニットIDをコードに組み込み、アプリ(プログラム)作成して、ストア公開
- ユーザがアプリ上で表示される広告をクリック
- 「その広告に紐づくユニットID⇨そのユニットIDに紐づくアプリID⇨そのアプリIDに紐づくGoogleアカウント」というように特定し、収益情報を管理する

Ionic×Capacitorで、AdMobのネイティブ広告を表示する方法
前提
- ネイティブ広告とは、広告がアプリに溶け込むように、アプリのUIに合わせて作れるような広告
- バナー・インタースティシャル・リワードはフォーマットが決まっている
- Capacitorは、標準では、バナー・インタースティシャル・リワードしかサポートしてないため、ネイティブ広告を表示させるには、独自pluginを作る必要がある

開発対象をandroidからiosに移行する
背景
- そもそもプライベートで使ってるスマホがiPhone(androidは持ってない)だしmacbookも持ってるのに、 わざわざandroid開発を始めた理由は、Apple Storeに公開するよりGoogle Play ストアに公開する方が安上がりだったから
- プライベートでお金が入りそう&Android Studioのエミュレータが重いので、実機で確認したいので、この際iOS開発に切り替えようと思った
- 会社のiOSアプリのリリース作業を手伝って、いろいろ知見ができた
- あと、Capacitorを試したかった(Capacitorなら androidからiOSへの開発の移行も楽だろう、と)
手順
発生した問題
- 「ビルド〜実機で確認」まではすんなりいった
1.アプリ背景が真っ黒に
- ただ、アプリを起動してみると、なぜかアプリの背景(background-color)が真っ黒に
- androidの時は、真っ白だった
- ダークモードのような感じ
1.対応
- 調べても同じ問題になってそうな人はいなかった
- 色々試した
- index.htmlの修正
- capacitor.config.tsの修正
- 結局、影響があったのは、以下のようなApp.cssを作成して、App.tsxで読み込むこと
ion-content { --background: white; --color: black; }
- このことから、おそらく原因は、App.tsxの中でいろいろIonic用に読み込んでるcssのうち、どれかにダークモードになってしまうやつがあるのではないかと推測

設計レベルで大幅な修正が必要になった
前提
- 本ゲームは、複数ステージで構成される
- 1ステージ : 1模擬悪質webサイト
- 模擬webサイトは、一般的なwebサイトと変わらないので、サブページがいくつもある
今の設計
- Stage型
export interface Stage { id: number; title: string; genre: string; theme: string; ratePerSecond: number; stepSequence: string[]; }
- stepSequenceは、そのステージのサプページをゴール(退会完了)までの順番で格納している
- つまり、トップ画面⇨退会確認画面⇨退会完了画面であれば、["top", "cofirm"]
- stepSequenceは、そのステージのサプページをゴール(退会完了)までの順番で格納している
- ステージランナーというコンポーネントを作り、そこでステージ、さらにサプページの描画を切り替える
- サプページの切り替えには、stepSequenceを利用する
- たとえば、topの退会ボタンが押されたら、stepSequenceを+1して、次のステップ(サプページ)に進むので、表示する内容はcofirmコンポーネントになる
- サプページの切り替えには、stepSequenceを利用する
- サプページコンポーネントも使い回す
- ステージ1でもステージ2でも、topが現在のステップなら、topコンポーネントを使用する(ステージごとにpropsを変える)
問題点
- サプページの中には、stepSequenceに入れられないものもあるので、そのようなページは別途でルーティングが必要
- たとえば、コンテンツ一覧ページや利用規約ページなどが直接的にゴールまでのステップの中には含まれない
- ステージランナーでの描画の切り替え+一部ルーティングの構成になってしまって、戻る操作に関して、バグや複雑な管理の必要が増えてきた
修正案
概要
- ルーティングに統一
- ルーティング対象が多くなっても、それはアプリ(ゲーム)の仕様上、仕方がないと割り切る
- 複雑な遷移管理をするよりはまし
- ただ、ルーティングをハードコードしたくないため、設定ファイルを使うのは維持し、さらにルーティングをある程度動的に作れるように、配列(マップ)を定義しておく
- ステージ追加の際は、設定ファイル+ステージ配列に追加する感じ
- さらに、全ステージのコンポーネントをインポートすることを避けるため、lazy importを使ってみることに
- サプページコンポーネントは、使い回すことをやめる
- サプページを構成する部品をコンポーネントにして、ステージごとにサプページコンポーネントを作成する
- したがって、サプページコンポーネントとサプページpathをマップとして作成する
修正版のフロー
ルーティング設計
パス | コンポーネント(表示内容) |
---|---|
/ | ゲームトップ |
/description | ゲーム説明 |
/stages | ステージ選択 |
/stages/{id} | 各ステージのトップページ |
/stages/{id}/{subpage} | 各ステージのサブページ |
/register | 全ステージ共通のユーザ登録 |
/result | 全ステージ共通の結果画面 |