マイブログ リスト

2024年10月18日金曜日

スマホアプリの課金システムを実装して大変だった話

初めてのブログです!🌷

最近は暑かったり寒かったり、ついこの前まで夏だったじゃん!って感じですね〜
実はまだ衣替えが追い付いてなくて、秋服に入れ替えている間に冬になりそうな雰囲気を感じております〜☃️

さて、突然ではありますが、
私自身、月島に入ってからスマホアプリの課金システムの実装を2回経験して、
課金実装の大変さを実感したので、その経験を軽めの感じでまとめてみようと思います💪

※始めに...この記事ちょこっと長めなのと、課金のかなーり表面的な上澄のようなお話なので、その二点、あらかじめご了承ください🙇‍♀️


早速課金システムのお話を始めていこうと思います🙇‍♀️

まず、課金システムといっても大きく分けて下記の2パターンの実装方針があります。

  • ストアフロント
    • すでに完成している課金システムを組み込む
    • 開発コストが抑えられる(システムの組み込みだけ)
    • 指定されている決済方法以外使えない(仕様変更に対応しずらい)
    • 通常の運用コスト(サーバーや人件費)にプラスでシステムの使用料がかかるため運用コストが上がる
  • フルスクラッチ
    • ゼロから全て自前実装
    • 開発コストがかかる
    • AppleやGoogleの仕様内であれば自由な設計ができるので、サービスの仕様変更にも対応しやすい
    • 通常の運用コスト(サーバーや人件費)で抑えられる

私が経験した課金システムの実装はいずれもフルスクラッチだったので、
今回はその視点から課金システムの実装の大まかなざっくり説明と、大変だったポイントをまとめてみます。


課金アイテムがあるスマホアプリを使ったことがある方はなんとなくイメージしやすいかもしれませんが、課金システムは大きく分けて2つの機能を実装する必要があります。

  • 都度課金実装
  • サブスクリプション実装


それぞれの概要を簡単に説明するとこんな感じになります。💰

【都度課金】

  • 買い切りのアイテム
  • お金がかかるのはその時のみ

【サブスクリプション】(以下サブスク)

  • 定期購読
  • 購入したサブスクに設定された期間毎に自動で更新・請求される


続いて、上記課金アイテムの主な処理は下記のようになります👇

【都度課金】

ユーザー購入時:

  • ユーザーが課金アイテムを購入
  • ストアで購入処理が完了
  • レシート情報が受け取れる
  • レシート情報を検証(正しくない情報の場合は処理しない)
  • 該当のアイテム等をユーザーに付与する

【サブスク

ユーザー購入時:

  • ユーザーが課金アイテムを購入
  • ストアで購入処理が完了
  • レシート情報が受け取れる
  • レシート情報を検証(正しくない情報の場合は処理しない)
  • 該当のサブスク情報のステータスをユーザーに付与する

更新時:

  • ストア側でユーザーの購入処理が完了
  • webhookから更新したレシート情報が送られてくる
  • 受け取ったレシート情報を検証する(正しくない場合は処理しない)
  • レシート情報から該当のユーザー情報を取得する
  • レシート情報をチェックして、正しいステータスに更新する
    • この時に解約している場合もある

定期バッチ更新:

  • 更新漏れがないかチェック
  • 課金ユーザーの最新のレシート情報を取得
  • レシート情報を検証
  • 受け取ったレシート情報を元にユーザーのステータスと照合
  • 正しくないステータスのユーザーがいた場合は正しいステータスに更新する

この説明から分かるように、サブスクの実装は単純に処理することが多いのと、ユーザーのステータスを保持しておかないといけないのでその点も実装上でややこしくなってくるポイントになります。


都度課金の実装については、おおよそシンプルなものになるので、今回は省略して、これ以降の「課金システム」というのは主にサブスクについてのまとめになります。


ます、サブスクはその仕様によって難易度が天と地ほどの差が出てきます。

サブスクの仕様について、パッと思いつく限りのだと

  1. 1つのプランのみのパターン
    • (例:スタンダードプランなど)
  2. 2つ以上のプランで、グレード(金額)が異なるパターン
    • (例:スタンダードプラン、プレミアムプランなど)
  3. 2つ以上のプランで、グレードは同じだが期間が異なるパターン
    • (例:1ヶ月、3ヶ月、6ヶ月、のプランがあるやつ)
こんなパターンの仕様があるかなと思います。
難易度的には、体感「1<<<<2<3」みたいな感じかなと🤔
アップグレードやダウングレードなどのプラン変更の概念が絡んでくると一気に実装が難しくなる印象です。


ここから少しだけ実装面のお話になります。(軽ーくなので、詳細は省きます🙇‍♀️)

【おすすめのサーバー構成】

サーバーの構成は実装が少しだけ楽になるものを記載してます!

※AWSを使うと想定した場合で文言使います

  • APIサーバー
  • バッチサーバー
  • webhookサーバー
  • DB
  • メイン処理サーバー (SQSとLambdaの連携)
ざっくりとしたシーケンス図載せておきます🙇‍♀️(かなり省略気味...)

【購入処理】

【webhook処理】

【バッチ処理】


このサーバー構成がおすすめの理由としては、

  • SQSとLambdaを使うとDBの処理をまとめられる!
  • 仕様変更などにも対応しやすい
  • コード量が少なくなる
  • デッドロックを回避できる

となります。

APIサーバー、バッチサーバー、webhookサーバーそれぞれでデータの更新処理をするパターンも実装しましたが、仕様変更やバグ修正の対応の際に修正漏れがあったりしてかなり大変でした。
なので、SQSなどのメッセージ処理のサービスを使って、Lambdaなどで処理をまとめてしまうのが圧倒的に楽(なはず)です!


ここで、私が実装した時のストアから受け取れる、レシート情報とwebhookの通知を少しだけ簡単にまとめます🍋(レシート情報は変更が入る可能性が高いので最新の情報は公式HPを参考にしてくだいさい!)

【レシート情報】

Google:

  • linkedPurchaseToken(一連の購入処理で一意、解約すると変わる)
  • purchaseToken(購入ごとに一意。購入ごとに変わる)
  • startTimeMillis(購読開始日時。サブスク最初に購入した日時)
  • expiryTimeMillis(購読の有効期限。次の更新タイミング)

Apple:

  • originalTransactionId(ユーザーごとに一意、アップルアカウントに紐づくので解約しても変わらない)
  • transactionId(購入ごとに一意。購入ごとに変わる)
  • original_purchase_date(サブスクを最初に購入した日時)
  • purchase_date(購読購入/更新日時)
  • expires_date(購読の有効期限。次の更新タイミング)

レシート情報の中身は、普通にスーパーなどで買い物をした時にもらうレシートを思い出していただくと、イメージしやすいかと思います!
お店毎にレシートの書き方が違うように、課金システムで各ストアから受け取れるレシート情報もGoogleとAppleでレシートの内容が違うのがわかるかと思います....
ユーザーごとのデータも持ち方が変わってくるので、その管理方法のテーブル設計もよく考える必要がありそうです。


次に、サブスクの更新時にwebhookから送られてくる通知情報を軽く説明します。

【webhookの通知情報】

Google:

  • (1)SUBSCRIPTION_RECOVERED
    • 定期購入がアカウントの一時停止から復帰した。
  • (2)SUBSCRIPTION_RENEWED
    • サブスクの定期更新時に来る通知。
    • 一番良く来る通知の種類。
    • アクティブな定期購入が更新された。
  • (3)SUBSCRIPTION_CANCELED
    • サブスクがユーザーまたはシステム側でキャンセルされた。
    • 自発的なキャンセルの場合、ユーザーがキャンセルしたときに送信されます。
  • (4)SUBSCRIPTION_PURCHASED
    • 新しいサブスクが購入された。
    • サブスク解約後、期限が切れてからの再購入もこの通知。
  • (5)SUBSCRIPTION_ON_HOLD
    • アカウントの一時停止を許可している場合にのみ来る通知。
    • サブスクでアカウントが一時停止された。
  • (6)SUBSCRIPTION_IN_GRACE_PERIOD 
    • 猶予期間の設定をしている場合にのみ来る通知。
    • サブスクが猶予期間に入った。
  • (7)SUBSCRIPTION_RESTARTED
    • ユーザーが [Play] > [アカウント] > [定期購入] からサブスクを再開した。
    • サブスク解約後、ユーザーが再開した時点でまだ期限切れになっていない場合に来る通知
  • (8)SUBSCRIPTION_PRICE_CHANGE_CONFIRMED
    • サブスクの料金改訂時に対象ユーザーに通知をした場合に来る可能性がある通知。
    • サブスクの料金変更がユーザーによって確認された。
  • (9)SUBSCRIPTION_DEFERRED
    • サブスクの契約期間が延長された。
  • (10)SUBSCRIPTION_PAUSED
    • サブスクが一時停止された。
  • (11)SUBSCRIPTION_PAUSE_SCHEDULE_CHANGED
    • サブスクの一時停止スケジュールが変更された。
  • (12)SUBSCRIPTION_REVOKED
    • 有効期限前にユーザーがサブスクを取り消した。
  • (13)SUBSCRIPTION_EXPIRED
    • ユーザーが解約した後、期限切れになったタイミングで来る通知。
    • プラン変更でアップグレードをした時に、前のプランがサブスク有効期限切れとしてこの通知も来る。
    • サブスクが期限切れになった。
  • (20)SUBSCRIPTION_PENDING_PURCHASE_CANCELED
    • 保留中の取引 は解約されました。

Apple:

  • INITIAL_BUY
    • サブスクの初回購入時に来る通知。
    • 同じアップルアカウントでは1度しか来ない通知。
  • CANCEL
    • Appleカスタマーサポートによって返金などの理由で解約されたとき
    • または別のサブスクへアップグレードしたとき
    • ユーザーが手動で購読の自動更新を停止した場合は来ない。
  • RENEWAL
    • 過去に更新に失敗した期限切れのサブスの自動更新が成功したとき 
    • deprecateなので、今は来ない通知のはず。
  • INTERACTIVE_RENEWAL
    • ユーザーがアプリ内、またはApp Storeからサブスクを更新したとき
  • DID_CHANGE_RENEWAL_STATUS
    • サブスクの更新ステータスが変更されたとき
    • 解約したときにも通知される
    • プラン変更の場合はこの通知が来る。
  • DID_CHANGE_RENEWAL_PREF
    • ユーザーがサブスクリプションプランを変更したとき
  • DID_FAIL_TO_RENEW
    • 購読中のサブスクの更新時に、決済の問題で更新に失敗したとき
  • DID_RECOVER
    • 更新に失敗した期限切れのサブスクの自動更新が成功したとき
  • PRICE_INCREASE_CONSENT
    • サブスクリプションの価格が値上げされるとき
    • この通知の際に受け取れるレシート情報で、ユーザーが値上げに同意したかどうかが分かる
  • DID_RENEW
    • サブスクの定期更新時に来る通知。
    • 一番良く来る通知の種類。
    • サブスクリプションの自動更新が成功したとき
  • REFUND
    • App Storeからユーザーへの払い戻しがあった時に来る通知
    • sandboxではテスト不可

通知の種類をざっと簡単に書きましたが、googleとappleそれぞれで10個以上の通知の種類があり、それに対応した実装とテストが必要となるため、これも大変な部分になります...😭


では実装面のお話はこれくらいにして、ここから話を少し変えて、実装を経験して大変だったポイントをまとめていこうと思います!

【課金実装での大変なポイント】

  • 各ストアで仕様が異なる
    • 先ほどのレシート情報の通り、レシートの情報が各ストアで違うので、OSごとにユーザーのデータの持ち方が異なってきます
  • 金額設定の最低金額が違う
    • Googleは99円
    • Appleは160円(2022年に120円から値上げ)
    • つまり、iosもandroidも対応する場合は最低金額は160円以上に設定する必要があります
  • sandboxが想定通りに動かない
    • 同じアカウントを使い回すと、想定通りに動かないことがあリマス
    • 払い戻しや支払い失敗などのマイナー系のテストがsandboxではできないです
  • テスト用アカウントは基本使い捨て(最新の情報は不明)
    • テスト用のアカウントを使い回すとなぜか新規扱いになったり、すでに課金アイテムを購入したことがあるユーザー判定されてしまったりと安定しないことが多かったです
    • appleでは、同端末で違うappleアカウントを使っていても、同一ユーザーとみなされてしまうことがあったので、iphoneでのテストは1回テストをしたら一定時間おいてから次のテストをしてました...
    • 以前はgoogleアカウントをいくつも作成することができたが、今は1端末1アカウントになっているようなので、この辺りの解決方法はまたそのうち考えようと思います
  • googleの残金充当期間についての理解
    • サブスクでアップグレードをした際に、前のサブスクのグレードの残金分(アップグレードしなかったら継続されていた期間分)をアップグレードしたプランに充てて、プランを切り替えるもの
    • Appleにはない概念(appleはアップグレードしたら残金分は払い戻される)
    • そもそも両ストアともの仕様で、アップグレードしたら即時アップグレードプランへの切り替えをしなければならないです
    • ※残金充当期間についてはややこしいので、機会があれば別の記事で詳しく説明しようと思います!!!

大変なポイントはまだまだありますが、大きなものであげるとこんな感じでした。

以上が、私が課金システムの実装をして得た知識(ほんの一部)と大変だったポイントのお話でした☺️


今回はフルスクラッチ実装のお話でしたが、フルスクラッチは単純に時間と労力がかかるものなので、既存の課金システムのサービス(もちろん有料)を使える場合は、そちらを使うことも検討してみるといいかもしれません。

とにかくオリジナルの課金実装は時間がかかります...
想像の倍はかかると思っていてもらってもいいかもしれません😵
テストも含めて、半年〜1年くらいは実装期間見ておくと少し余裕が持てるかもと思いましたが、サブスクの仕様にもよるので一概には言えないところでもあります。

ちなみに、課金システムについては、どんどん情報が更新されていって、2年前と現在でもレシート情報がアップデートされていたりするので、情報を追うのも実際に実装をするのもとても大変だと思います。
appleだと、レシート情報のバージョンがv1→v2にアップデートされて、v1で使っていたレシート情報や通知が非推奨になっていたり使えなかったりすることが多々あります。
ただ、課金はかなり奥が深く、ユーザーが使う機能に直結する部分なので、とてもやりがいは感じますし、私自身は課金システムにかなり引き込まれてしまいました☺️
また機会があれば課金システムの実装に携わりたいと思いますし、もっと知識を深めていきたいなと思っています!

最後になりますが、ここまで読んでいただいてありがとうございました!🌷

2024年9月13日金曜日

C#の公式ドキュメントを通して読んでみた。

 

以前、Unityの公式ドキュメントを通して読んでみて(以前の記事はコチラ)、結構学びがあったので、今回はC#言語そのもののドキュメントを読んでみたいと思います。

実際に読んだのは、こちらページです。
https://learn.microsoft.com/ja-jp/dotnet/csharp/tour-of-csharp/overview





サイトの構造が分かりにくいのですが、サイドバーの「はじめに」~「C#プログラミングガイド」まで、ざっくりと目を通しました。


ただし、Unityドキュメントで既に読んだことが大部分で、それ以外の部分はUnityではまだ扱えなかったりするので、業務上の収穫は少なめでした。

面白いなと思った部分を下記に共有したいと思います。



1.Delegateの設計ミス

・対象ページ:System.Delegate と delegate キーワード

Delegeteとは、ごく簡単に言うと、関数を保存しておける変数のようなもの(私訳)

C#では関数を1つ保存できるDelegeteと、複数同時に保存できるMulticastDeleteが明確に区別されて設計されていますが、プログラミングの実用上、この2つは曖昧になってきたらしいです。

そこで下記の一言。

この区別は、実際には当初考えていたよりも役に立たないことがわかりました。 

これは脆弱性を生んだりするような設計ミスではありませんが、公式ドキュメント上に間違いを認めるような文言が載っている、というのは珍しい感じがして、ちょっと面白くないですか?(私だけ?)



2.パターンマッチング

・対象ページ:パターンマッチング

「あ、これ使いたいかも」と直感的に感じた機能。
※誤解を生みやすい名前ですが、Regex.Match()とは別物です。


公式の例はコードコチラ


    int? maybe = 12;

    if (maybe is int number)
    {
        Console.WriteLine($"The nullable int 'maybe' has the value {number}");
    }
    else
    {
        Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
    }
   

if条件がかなり口語的で分かりやすく、nullチェックをしながら数値に変換できる、と凄く使いやすそうな感じがします。


そしてそれを使ったSwitch文がコチラ


    public State PerformOperation(string command) =>
        command switch
        {
            "SystemTest" => RunDiagnostics(),
            "Start" => StartSystem(),
            "Stop" => StopSystem(),
            "Reset" => ResetToReady(),
            _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
        };

めちゃめちゃ見やすくないですか?

いちいちbreak;を書かずに、処理の分岐を羅列できる。
まさにSwitch文の「ブレークスルー」やぁ~(古い)


3.レコード

・対象ページ:レコード

レコードとは、DBのデータのようなものを扱いやすいように、少し機能を加えた特殊なクラスです(私訳)

classの代わりにrecordとし、宣言すると、


    public record Person(string FirstName, string LastName);

それだけで、下記のボイラープレートコードを勝手に実装したクラスが出来上がります。

・Equals()

・GetHashCord()

・すべてのプロパティを引数にしたコンストラクタ


レコードはC#9.0で実装された機能のため、Unity目線では新しい技術です。

C#10で機能追加されてレコード構造体がサポートされたりと、進化を残している部分もあります。

(現在のUnityはC#9.0まで対応)






2024年8月15日木曜日

SpringBoot+MysqlのWebアプリをAWSに無料デプロイする

    

 おはようございます。

最近、SpringBootでwebアプリを作っていて、いい感じになりました。
というわけで、どこか無料のサーバーを借りて公開したいという気持ちになりまして。
いろいろ調べてみたところ、WebサーバもDBも、まとめてAWSにデプロイできたので、この場を借りて紹介したいと思います。

まずは、今回作ったwebアプリを軽く説明します。

その名は「キメラ召喚バトル」

入力した文字列から、脳内メーカーの要領でキメラを生み出し、戦わせるという内容。
ユーザー情報や、生まれたキメラの情報はDBに保存しています。

バージョン等は下記の通りです。

  • SpringBoot 3.3.2
  • Java 17
  • MySQL 8


次にAWSについて、荒く短く説明します。

AWS(Amazon Web Service)とは?

いろいろなサービスを提供します。
(多すぎて説明できないので、以下に今回使うものだけ解説します)
  • EC2(Elastic Compute Cloud):サーバを借りられるサービス。今回はWebサーバとして利用。
  • RDS(Relational Database Service):DBを借りられるサービス。今回はMySQLにします。
  • Elastic Beanstalk:簡単にデプロイできるサービス。EC2とRDSの連携を自動的にやってくれる。
ちゃんと知ってる人はツッコミたい部分もあると思いますが、今回の用途だと大体こんな感じです。
上記の利用料金ですが、Elastic Beanstalkは無料、EC2とRDSは1年分の無料枠が用意される設定があり、間違えなければ無料でデプロイできます。

(→私は間違えたので料金請求されました。詳しくは後ほど)


目標設定

AWSには設定項目がたくさんありますが、

「無料で簡単にとりあえずデプロイすること」

を目標にやりますので、細かい部分は触れないようにします。

前提条件

SpringBoot+MySQLでアプリを作り、ローカルで動作できていることを前提にします。

事前準備

Spring側の application.propertiesの設定を変更します。

DBを使っている場合、おそらく下記のようになっていると思います。
(Dockerなどでローカルに建てたDBにアクセスしている想定)


    spring.datasource.url=jdbc:mysql://localhost:3306/my_database
    spring.datasource.username=user
    spring.datasource.password=password
   

それを下記のように追記します。


    spring.datasource.url=jdbc:mysql://${RDS_HOSTNAME:localhost}:${RDS_PORT:3306}/${RDS_DB_NAME:my_database}
    spring.datasource.username=${RDS_USERNAME:user}
    spring.datasource.password=${RDS_PASSWORD:password}
   

意味としては、後に設定するRDSの環境変数を確認し、あればそっちの値を参照するというようなものです。
(ローカル動作の場合には、環境変数が存在しないのでコロン以下のこれまでの定数値を参照できます)

この変更を加えたら、Spring側はjarファイルをビルドしておきます。

AWSの準備

まず登録してログインします。(割愛)

次に、訳あって事前にロールの作成を行う必要があります。
理由は後に説明しますので、とりあえず下記の通りに作成しておいてください。

すべてのサービス > セキュリティ、ID、およびコンプライアンス > IAM を選びます。
(画像右下)

その後、左メニューからアクセス管理 > ロールを選択。
ロール画面右上の「ロールを作成」をクリックして、以下の通り入力していきます。
  • 信頼されたエンティティタイプ:AWSのサービス
  • サービスまたはユースケース:EC2 
  • ユースケース:EC2
「次へ」を押すと、大量のポリシーが表示されているので、検索に「beans」と入力して絞り込み、下記の3つを選択して「次へ」を押します。
  • AWSElasticBeanstalkMulticontainerDocker
  • AWSElasticBeanstalkWebTier
  • AWSElasticBeanstalkWorkerTier


ロール名を「aws-elasticbeanstalk-ec2-role」とし、画面下の「ロールを作成」
これでロールの作成はOKです。

ElasticBeanstalk環境作成

すべてのサービス > コンピューティング > Elastic Beanstalkを選びます。

「環境を作成」をクリックすると、合計6ステップの入力画面に移行するので、
下記のように設定していきます。(デフォルトで良いものは記載省いています)



Step1

  • アプリケーション情報
    • 任意のアプリケーション名を入力
  • プラットフォーム
    • プラットフォーム:Java
    • ブランチ:Corretto 17 (javaのバージョンに合わせる)
  • アプリケーションコード
    • コードをアップロードを選択
    • ラベル名:任意のバージョン名を入力
    • ローカルファイルを選択し、Springのjarファイルをアップロード。

Step2

  • サービスアクセス
    • サービスロール:新しいサービスロールを作成して利用
      • 名前は「aws-elasticbeanstalk-service-role」のままでOK
    • EC2インスタンスプロファイル
      • 作成した「aws-elasticbeanstalk-ec2-role」を選択
※以前はEC2のロールも自動作成されていたらしいのですが、セキュリティの都合でそれはできなくなったそうです。だから、自分で作る必要があったんですね。→
参考サイト

Step3

  • データべース
    • データベースを有効化:true
    • インスタンスクラス (要注意ポイント!!
      • 「db.t3.micro」に変更。
    • ユーザー名
      • 任意で入力
    • パスワード
      • 任意で入力

インスタンスクラスのデフォルトが「db.t3.small」になっていますが、これには無料枠がありません。無料で済ませたい場合は「db.t3.micro」に必ず変更してください。

ユーザー名・パスワードは、環境変数を通じてSpring側に共有されるので、好きに決めて問題ありません。

Step4

  • 容量
    • インスタンスタイプ:t3.microを残して、t3.smallを削除
コチラも、無料枠はt3.microにだけ用意されているので、t3.smallは削除しておきます。

Step5

  • プラットフォームソフトウェア
    • 環境プロパティ
      • PORT:8080を追加

spring側のポートは8080がデフォルト、nginxは5000がデフォルトのため、
spring側がデフォルトの場合は、「環境プロパティを追加」を押して、
「名前:PORT、値:8080」を設定します。

Step6

Step6は確認なので、問題なければ「送信」をクリック。

環境の構築と立ち上げに少し時間がかかります。
終わり次第、ドメインと表記されたURLからデプロイされたWebアプリにアクセスできます。

DBのテーブル作成などは、Spring側で「spring.jpa.hibernate.ddl-auto」を設定しておけば、自動作成してくれます。事前にローカルで検証してください。

DBを手動で操作する場合は、EC2のインスタンスに接続し、そこを足掛かりにしてRDSのエンドポイントに接続する形になります。(詳細は省きます)


無料枠の落とし穴

最後に、無料になっているかを確認してみましょう。
Billing and Cost Management にアクセスし、コストサマリー下部の「請求書を表示」をクリック。そこで下にスクロールすることで、サービス別料金が表示され、何に料金が発生しているかを確認できます。
私は、下記の2点で失敗しておりました。

1.インスタンスの選び方
EC2、RDSのインスタンスの初期値がsmallになっているので、microに変える必要がある。
こちらは上記の手順の中で説明したとおりです。

2.パブリックIPv4
これまでの手順でRDSのインスタンスを作った時、デフォルトで「パブリックアクセス可能」の設定になってしまうようです。
パブリックIPv4は、外部からのアクセスするための設定であり、近年有料化(0.005$/hrs)された機能です。EC2の方には無料枠がありますが、RDSは有料でした。
Elastic BeanstalkではデフォルトでONになってしまってしまうようなので、RDSのインスタンスを直接編集して手動でOFFにしてください。



以上です。
今回の手順では、パブリックIPv4の関係で厳密には完全無料ではなく$0.005くらいはかかってしまうかもしれませんが、許してください。私はよく分からないうちに$9.43の請求になってました。

記事を書いてみての感想ですが、他の人が再現できるように手順を構成するのは、なかなか大変でした。
アプリをデプロイするまでに、ここには書かれていない様々なトライ&エラーがあったわけですが、改めてそれを1つ1つ検証して、必要なものとそうでないものに分けていくという過程を経ることで、AWSへの理解が倍くらいに深まったと思います。
  • Lv0 なにもできない
  • Lv1 よくわからんけどできた
  • Lv2 どうすればできるか分かった(今ココ!)
という感じです。

最後に、作成した「キメラ召喚バトル」のリンクを貼っておきます。

無料枠は1年しかないので、枠が切れる頃には終了する予定です。

2024年7月23日火曜日

透過pngの余白を自動調整するプログラムを作ってみた

おはようございます。
最近、キメラを召喚して対戦するゲームを作っています。

キメラの画像はコチラの透過pngの素材を使わせていただいているのですが、
どうにも余白が不揃い(全体的に右寄り)で、少し扱いづらい。
(でもメッチャ可愛いし使いたい)

なので、余白の自動調整をするプログラムを作ります。

ロジックとしては、

1.画像を読み込む
2.余白を全カットする
3.いい感じに余白をつけなおす
4.出力する

という想定。


というわけで、調査開始。

調べてみると、ちょうどいい記事を発見。
【画像処理】OpenCVを使って画像周りの余白削除(トリミング)を自動化してみた!(1)

pythonは3日くらいしか触っていませんが、何となくopenCVをインストールして、ところどころ修正を入れながら、適当な画像で変換開始!



うん、真っ黒だね!!!


コードを触って確かめていくと、どうやら最初の画像読み込み(cv2.imread)の時点で、真っ黒になっているらしく、透過pngの場合には「flags = cv2.IMREAD_UNCHANGED」が必要だったらしい。


    # 画像の読み込み
    img = cv2.imread(image, cv2.IMREAD_UNCHANGED)


オプションをつけてから、読み込んだ画像をそのまま吐き出すと、無事に透過pngが扱えることを確認!

良かった~、と安心して、

修正したプログラムを再度実行!!




うん、真っ黒だね!!!

今度はなんだ(# ゚Д゚)!?

どうやら、グレースケール・2値化したときに、やっぱり真っ黒になっている。
現状のコードではダメらしい(知らんけど)

ロジックとしては、地と図で2値化できればいいはずなので、ChatGPTに聞いてみる。


(こういう限定的な用途での実装コードは結構頼れるなぁ)


    # アルファチャンネルを抽出
    alpha_channel = img[:, :, 3]

    # 透明部分を黒(0)、不透明部分を白(255)に二値化
    _, binary_mask = cv2.threshold(alpha_channel, 0, 255, cv2.THRESH_BINARY)


上記のコードで、いい感じに2値化できるらしい。

というわけで修正したコードを実行。


余白ゼロ、超コンパクト!
いい感じだ( *´艸`)


最後に、余白をいい感じに追加

余白の追加は、cv2.copyMakeBorder()でできるらしいので、
横は中央揃え、縦は下揃えとして適当に計算するコードを追加。
(めんどくさいので全部直書き)


    #完成画像のサイズ定数
    TARGET_HEIGHT = 1300
    TARGET_WIDTH = 1300
    BOTTOM_MARGIN = 100

    # 余白の計算(bottom固定、左右中央揃え)
    crop_h, crop_w = crop_img.shape[:2]
    bm = BOTTOM_MARGIN
    tm = TARGET_HEIGHT - crop_h - bm
    lm = (TARGET_WIDTH - crop_w)//2
    rm = TARGET_WIDTH - lm - crop_w

    # 余白が足りない場合のエラー表示(要:定数の調整)
    if(tm < 0 | lm < 0):
        print("error:" + image_name)
        continue

    # 余白の追加
    margined_image = cv2.copyMakeBorder(crop_img, tm, bm, lm, rm,
                                         cv2.BORDER_CONSTANT, (0,0,0,0))


実行結果

Before

After




プログラムの完成版はこちら


import cv2
import os
import glob

# 余白を削除する関数
def crop(image): #引数は画像の相対パス
    # 画像の読み込み
    img = cv2.imread(image, cv2.IMREAD_UNCHANGED)

    # アルファチャンネルを抽出
    alpha_channel = img[:, :, 3]

    # 透明部分を黒(0)、不透明部分を白(255)に二値化
    _, binary_mask = cv2.threshold(alpha_channel, 0, 255, cv2.THRESH_BINARY)

    # 輪郭を抽出
    contours = cv2.findContours(binary_mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]

    # 輪郭の座標をリストに代入していく
    x1 = [] #x座標の最小値
    y1 = [] #y座標の最小値
    x2 = [] #x座標の最大値
    y2 = [] #y座標の最大値
    for i in range(0, len(contours)):
        ret = cv2.boundingRect(contours[i])
        x1.append(ret[0])
        y1.append(ret[1])
        x2.append(ret[0] + ret[2])
        y2.append(ret[1] + ret[3])

    # 輪郭の一番外枠を切り抜き(※5pxの余裕を持たせた)
    x1_min = min(x1)-5
    y1_min = min(y1)-5
    x2_max = max(x2)+5
    y2_max = max(y2)+5
    cv2.rectangle(img, (x1_min, y1_min), (x2_max, y2_max), (0, 255, 0), 3)

    crop_img = img[y1_min:y2_max, x1_min:x2_max]

    return img, crop_img

#フォルダ名、拡張子
INPUTDIR = 'images_fot_trim'
OUTPUTDIR = 'trimmed_images'
EXT = 'png'

#完成画像のサイズ定数
TARGET_HEIGHT = 1300
TARGET_WIDTH = 1300
BOTTOM_MARGIN = 100

# 編集後の画像の保存ディレクトリの作成
if not os.path.isdir(OUTPUTDIR):
    os.mkdir(OUTPUTDIR)

# INPUTDIR内の全ての画像に対してループ
for image in glob.glob(INPUTDIR + '/*.' + EXT):

    # 相対パスの部分を削除
    image_name = os.path.basename(image)

    # クロップ
    img, crop_img = crop(image)

    # 余白の計算(bottom固定、左右中央揃え)
    crop_h, crop_w = crop_img.shape[:2]
    bm = BOTTOM_MARGIN
    tm = TARGET_HEIGHT - crop_h - bm
    lm = (TARGET_WIDTH - crop_w)//2
    rm = TARGET_WIDTH - lm - crop_w

    # 余白が足りない場合のエラー表示(要:定数の調整)
    if(tm < 0 | lm < 0):
        print("error:" + image_name)
        continue

    # 余白の追加
    margined_image = cv2.copyMakeBorder(crop_img, tm, bm, lm, rm,
                                         cv2.BORDER_CONSTANT, (0,0,0,0))

    # 切り取った画像を保存
    cv2.imwrite(OUTPUTDIR + '/' + image_name, margined_image)

    print(image_name + " finished")

print("margin adjustment completed!!")



余談

現状の処理ではモンスターの左右端から同じだけの余白を入れているが、
人の目で見て「中央に揃っている」という印象になるためには、画像の重心が中央にくる必要がありそうだ。
画像の2値化はできているので、x軸に沿って画素を数えることで重心を算出すれば、さらに良くなりそう。

2024年2月5日月曜日

Unity公式ドキュメントを通して読んでみた

つよつよエンジニアになるには、公式ドキュメントを読むことが大事だと、誰かが言っていた。

というわけで、仕事で主に扱っている『Unity』の公式ドキュメントを通読してみた。

→実際読んだのはコチラ(2021.3)


サイドバーには23の項目があるが、それぞれの項目に深いネストがあり、機械的に計測すると1960のページがあるよう……。

関係ないところは飛ばしつつ、1か月以上かけて流し読みしたので、役立ちそうな部分や面白かった部分を紹介してみます。


読んでいる際の筆者の指向性は以下の3点。

・Unityならではの仕組みを勉強し直す。

・Unityを使ってAndroid+iOSのアプリを作ることを想定(ゲームに限らず)

・個人的に面白いと思ったことは調べてみる。


では、おすすめのページを以下に記していきます。


プロパティの編集

特にNumeric field expressionsについて。
複数のオブジェクトを規則的に並べたいとき、例えば、position.xにL(0,10)に入力すると、その範囲で規則的に配置してくれる。
手動でUIを均等配置したいとき、覚えておくと使えそう。

ガベージコレクションのベストプラクティス
Unityにおける最適化

Unityのパフォーマンスを上げるうえでの基礎知識。
中でも「文字列とテキスト」の文字列の連結処理に関する注意書きは、自分でも引っかかったので感慨深い。
for文などで、str += "add string" のようなコードをループ処理すると、大量のメモリを消費するため、GCが起きて一瞬止まることになる。
そういうときはStringBuilderを使いましょう。

特殊なフォルダ名

Assets
Editor
Resources
StreamingAssets
など、見たことあるフォルダ名は、Unityの予約語でした。
どういう役割や機能を持っていたのか、
もしも知らずに名前変えてしまうと痛い目に合いそうなので、1度目を通しておくと安心です。

iOS用のプラグインをビルド

iOS用プラグインのお作法について。
プラグインをそのまま使うならいいけど、改造するときなどは最初のとっかかりとして、この辺りを知っておきたい。

スクリプティングの概念

おそらくプログラマの本業にもっとも使う領域
実務経験があるなら、ほとんど知っているとは思うが、
せめてこのカテゴリ以下については目を通しておきたい。

この項目を知らなくてもコードは書ける。
だが、知らずに自分でcommon関数を作らないためにも見ておきたい。
例えば、「ある数値をmin-maxの間に納めたい」という場合、
if(x < min){ x = min; }
if(x > max){ x = max; }
みたいなコードを書くこともできますが、
実はそういうユーティリティは、ここに入ってるので、下記関数で一発。
x = Mathf.Clamp(x,min,max);
(私はこれまで、Clamp関数を何度も自作しました……。反省。)





ー番外編ー
プログラマは触らないかもだけど、個人的に面白かった仕組み2戦。

法線マップ(バンプマップ)

3D空間を扱うとき「ポリゴンが形を作って、テクスチャを張り付けて」というのが基本のイメージだけど、細かい凹凸パターンをポリゴンで真面目に作るとデータ量がマッハ。
大まかな形はポリゴンで作り、テクスチャ側で凹凸をつける仕組みが法線マップ。
ポリゴン上は平面なのに、ちゃんと影ができるぞ!

色空間

リニア色空間とガンマ色空間について。
光の強さを0.0から1.0まで均等に割り振ったのがリニア色空間ですが、
人間の目は、明るさに対してよく反応するため、白が強く見えてしまいます。
そこに補正を加えたものがガンマ色空間です。
「なんか思ったより明るい/暗い」という時には、この辺りが怪しいかも。


以上。

他にもシェーダとか、URPとか、知っておきたい項目はいろいろあるので、
通読でなくても、ちょこちょこ公式ドキュメントを読みましょう。
今すぐどこかで使えるものではないけども、いつかどこかで役に立つはず。
どんな分野にも基礎練習ってあると思いますが、プログラムにおいてはこういうことなのかも?