Open10

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

麽迦論麽迦論

作るもの

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

目標

  • GooglePlayストアに公開して、広告収入で収益化してみる
    • 月に500円稼げれば、自分が悪質サイトに払ってる分を相殺できる
麽迦論麽迦論

開発〜GooglePlayストア公開までの流れ

  1. 開発
  2. Google AdMobに登録
  3. アプリに広告コードを組み込む
  4. アプリのビルド
  5. Google Play Consoleに登録($25)
  6. 審査
  7. 公開
麽迦論麽迦論

開発環境構築

Android Studio

  1. 以下サイトからインストーラーをダウンロードし、インストーラー実行(ウィザードはすべてデフォルト値でインストール)
  2. 実行して、初回起動でSDKなどをダウンロード&インストール
    • とりあえず標準インストール

Ionicプロジェクト

  1. 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等)揃えるより楽だった
  2. Reactアプリを静的ファイルにビルドする

    ionic build
    
  3. Androidプラットフォームの追加

    ionic cap add android
    
  4. Android Studioで開く

    ionic cap open android
    
麽迦論麽迦論

ソース変更->ビルド->確認する

  1. ソース変更(追加/修正)
  2. ブラウザで確認
    iconic serve
    
  3. ビルド
    ionic build
    
    もしくは
    npm run build
    
  4. Capacitorに反映
    npx cap copy android
    
    もしくは
    npx cap copy android
    
  5. Android Studioを開く
    npx cap open android
    
  6. 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での使い方

  1. zustandのインストール
    npm install zustand
    
  2. 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 })), // 状態の減算処理
     }));
    
  3. コンポーネントで使用
    // 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つ
    • バナー広告
    • インタースティシャル広告
    • リワード広告

広告の作り方から収益情報管理まで

  1. AdMobにGoogleアカウントでログイン
  2. AdMob内で、アプリを作成(アプリID発行)
  3. アプリ内に、広告ユニット(バナー、インタースティシャル、リワード、etc...)を作成(ユニットID発行)
  4. そのユニットIDをコードに組み込み、アプリ(プログラム)作成して、ストア公開
  5. ユーザがアプリ上で表示される広告をクリック
  6. 「その広告に紐づくユニット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を利用する
      • たとえば、topの退会ボタンが押されたら、stepSequenceを+1して、次のステップ(サプページ)に進むので、表示する内容はcofirmコンポーネントになる
  • サプページコンポーネントも使い回す
    • ステージ1でもステージ2でも、topが現在のステップなら、topコンポーネントを使用する(ステージごとにpropsを変える)

問題点

  • サプページの中には、stepSequenceに入れられないものもあるので、そのようなページは別途でルーティングが必要
    • たとえば、コンテンツ一覧ページや利用規約ページなどが直接的にゴールまでのステップの中には含まれない
  • ステージランナーでの描画の切り替え+一部ルーティングの構成になってしまって、戻る操作に関して、バグや複雑な管理の必要が増えてきた

修正案

概要

  • ルーティングに統一
    • ルーティング対象が多くなっても、それはアプリ(ゲーム)の仕様上、仕方がないと割り切る
    • 複雑な遷移管理をするよりはまし
  • ただ、ルーティングをハードコードしたくないため、設定ファイルを使うのは維持し、さらにルーティングをある程度動的に作れるように、配列(マップ)を定義しておく
    • ステージ追加の際は、設定ファイル+ステージ配列に追加する感じ
  • さらに、全ステージのコンポーネントをインポートすることを避けるため、lazy importを使ってみることに
  • サプページコンポーネントは、使い回すことをやめる
    • サプページを構成する部品をコンポーネントにして、ステージごとにサプページコンポーネントを作成する
    • したがって、サプページコンポーネントとサプページpathをマップとして作成する

修正版のフロー

ルーティング設計

パス コンポーネント(表示内容)
/ ゲームトップ
/description ゲーム説明
/stages ステージ選択
/stages/{id} 各ステージのトップページ
/stages/{id}/{subpage} 各ステージのサブページ
/register 全ステージ共通のユーザ登録
/result 全ステージ共通の結果画面