KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

カテゴリ: ノウハウ

この記事はKLab Advent Calendar 2017の18日目の記事です

皆さんこんにちは、 jukey17 です

私はクライアントサイドエンジニアとしてゲームのプロトタイプを作る仕事を何度か経験しているのですが、今回はその際に体得した自分なりのノウハウをまとめてみようと思います よろしくお願いします

はじめに

この記事では、Unityを使ったプロトタイプ開発をベースに少人数体制(プログラマ・プランナー・デザイナー各1名ずつなど)で開発を進めていった経験を元に話を進めていきます

なのでUnityを使ったゲーム開発の経験がある方を対象とした内容になっています

プロトタイプ開発の仕事での経験ベースの話ではありますが、個人制作や学校・サークルでのチーム製作にも置き換えて考えられるような内容にまとめているので、参考にして頂ければと思います

仕様を素早く実装する → 仕様の変化を先読みした実装をする

最初に どういった考え方でプロトタイプ開発をすべきか というまとめっぽい話から始めます

大枠としてこれを念頭に置いて開発を進めておきたいということが伝わればと思います

プロトタイプ開発は「どのようなゲームを作っていくべきかを模索する」段階であるため、仕様がキッチリと決まっていることはほぼありません

アレを試してみよう、コレを組み合わせてみよう、やっぱりソレは削ろう... このようにして目まぐるしく実装内容が変わっていくのがプロトタイプ開発です

そうなってくるとついつい「とにかく数をこなすために急いで実装しなければ」と考えてしまうのですが、そうしてしまうと使い回しの効かない書き捨て品質なコードを書いてしまいがちになってしまいます

1回実装して終わりであればその実装の速度を重視して書き捨て品質にコードを書いてもよいと思うのですが、プロトタイプ開発では複数の種類の実装をしたり、その中で幾つかの機能を使い回してみたりと、とにかく色々な要素を組み合わせて面白さを追求していくため1回実装して終わりということは絶対にありません

  • 「先週試して外した機能をもう一度復活させたい」
  • 「パターンAで調整したパラメータをパターンBでも使いたい」
  • 「別の機能で使っている演出をとりあえず使い回してほしい」
  • などなど...

プロトタイプ開発では上記のような試行錯誤が沢山繰り返されます(特に後半になればなるほどこのような内容が増えます)

期間内で継続して開発スピードを出すにはこれらの試行錯誤を如何に予測して捌き続けることができるかが鍵だと個人的には思っています

ということで、上記の例を元にこれらの考え方を持ってどのように開発を進めていくのかを紹介していきたいと思います

プロトタイプ開発でありがちなケースとその対応

「先週試して外した機能をもう一度復活させたい」

仕様の定まっていないプロトタイプ開発ならではのよくあるケースの一つです

これをシューティングゲームにて敵を追尾していく弾を発射するという仕様を例に2つのコードを比較してみましょう

  • サンプルコードA
class Player : MonoBehaviour
{
    [SerializeField] GameObject homingBulletPrefab;
    [SerializeField] float homingBulletProgress;
    [SerializeField] float homingSpeed;
    [SerializeField] GameObject homingFinishEffectPrefab;
    
    GameObject homingBulletObject;

    void Update()
    {
        // 追尾弾発射
        if (Input.GetKeyDown(KeyCode.H))
        {
            // 連続では撃てない
            if (homingBulletObject == null)
            {
                homingBulletObject = Instantiate(homingBulletPrefab);
                homingBulletProgress = 0.0f;
            }
        }

        // 追尾弾の処理
        if (homingBulletObject != null && enemy != null)
        {
            homingBulletObject.transform.position = Vector3.Lerp(transform.position, enemy.transform.position, homingBulletProgress);
            homingBulletProgress += Time.deltaTime * homingSpeed;

            if (homingBulletProgress > 1.0f)
            {
                Destroy(homingBulletObject);
                homingBulletObject = null;

                // 着弾したら自機に演出が出る
                var effect = Instantiate(homingFinishEffectPrefab);
                Destroy(effect, 2.0f);
            }
        }
    }
}
  • サンプルコードB
class Player : ShipBase
{
    // 追尾弾の処理は特になし
}

[ReqiredComponent(typeof(ShipBase))]
class HomingBulletFirer : MonoBehaviour
{
    [SerialzieField] HomingBullet homingBullet;
    [SerializeField] float speed;

    ShipBase parentShip;

    void Start()
    {
        parentShip = gameObject.GetComponent<ShipBase>();
    }

    void Update()
    {
        // 追尾弾発射
        if (Input.GetKeyDown(KeyCode.H))
        {
            // 連続では撃てない
            if (!homingBullet.IsActive)
            {
                homingBullet.Fire(parentShip.transform, parentShip.Target.transform, speed, OnLanded);
            }
        }
    }

    void OnLanded()
    {
        // 着弾したら自機に演出が出る
        EffectManager.SpawnPlayerHomingFinishEffect();
    }
}

class HomingBullet : MonoBehaviour
{
    public void Fire(Tranform start, Transform end, float speed, Action onLanded)
    {
        // 弾を発射する処理
    }
}

サンプルコードAでは追尾弾をそのまま自機(Player)クラスに書いていますが、サンプルコードBでは追尾弾を発射するクラスと追尾弾自身のクラスを分けて実装しています

サンプルコードAの場合は表題の対応をする場合は、該当コードをコメントアウトしておいてそれを解除するか、削除しておいてgitなどのバージョン管理システムから掘り起こして復活させなければなりません

サンプルコードBの場合は HomingBulletFirer クラスを Player クラスが付いている GameObject に付けたり外したりするだけで対応が可能です

今回のようなシンプルな実装内容であれば、サンプルコードAのような実装でも一括でコメントアウトするだけでなんとかなりそうですが、もっと処理が複雑になって色んな所に該当コードが散らばっていくとそう簡単にはいかなくなります

サンプルコードBのように 「機能単位でパーツ(部品)を分けておく」 ことで繰り返される試行錯誤にも素早く対応していくことが可能です

Unityではこのようなまとまった処理をパーツ(部品)として扱うコンポーネント指向を基本とした設計がなされているので、サンプルコードBのようなパーツを分けた実装がしやすい構造になっています

実装を進めながらドンドンとパーツを増やしていき、それらを付けたり・外したり・組み合わせたりするだけで素早く沢山のパターンを試せるような状況を作っていくようにしましょう!

「パターンAで調整したパラメータをパターンBでも使いたい」

プロトタイプ開発では複数パターンを用意して遊び比べることが多くこういった話もよくあるできごとです

こちらもシューティングゲームを題材に2つのサンプルコードでどのように対応すべきかを見ていきましょう

サンプルコードA

// 撃てる弾の種類が複数ある・ボムが使える自機
class PlayerA : MonoBehaviour
{
    [SerializeField] int baseHp;
    [SerializeField] int baseAttack;
    [SerializeField] float moveSpeed;
    [SerializeField] BulletType[] usableBulletTypes;
    [SerializeField] int maxBombCount;

    ...
}

// 撃てる弾は1種類で特殊なスキルが発動できる自機
class PlayerB : MonoBehaviour
{
    [SerializeField] int baseHp;
    [SerializeField] int baseAttack;
    [SerializeField] float moveSpeed;
    [SerializeField] float skillIntervalTime;

    ...
}

サンプルコードB

class PlayerParam : ScriptableObejct
{
    [SerializeField] int baseHp;
    [SerializeField] int baseAttack;
    [SerializeField] float moveSpeed;
}

// 撃てる弾の種類が複数ある・ボムが使える自機
class PlayerA : MonoBehaviour
{
    public void Initialize(PlayerParam initialParam, BulletType[] usableBulletTypes, int maxBombCount)
    {
        // パラメータの保持などの初期化処理
    }

    ...
}

// 撃てる弾は1種類で特殊なスキルが発動できる自機
class PlayerB : MonoBehaviour
{
    public void Initialize(PlayerParam initialParam, float skillIntervalTime)
    {
        // パラメータの保持などの初期化処理
    }

    ...
}

サンプルコードAでは2パターンの自機をクラスに必要なパラメータを SerializeField 属性を付けて定義していますが、サンプルコードBではパラメータクラスを用意して初期化処理を呼んだ際にそれを設定しています

どちらのコードも外からパラメータを設定できるようにしている点は同じですが、サンプルコードBではパラメータを専用のクラスに逃している点に注目してください

このようにロジック(自機の挙動を定義する Player) とデータ(自機のパラメータを定義する PlayerParam)に分けることでパラメータの使い回しが容易になります

今回は自機を対象としていますが、シューティングゲームであれば、敵や弾、ボムなどある程度の機能単位でデータを別クラスに逃しておくのをオススメします

このようにして先にデータを切り出しておくことで、ある程度内容が固まったあとにプランナーの人を中心にパラメータ調整を行うといったことになってもスムーズに移行することができます

Unityの場合、データとして扱うために最適化された ScriptableObject やJSONを扱うための JsonUtility などがあるのでそれを活用しましょう!

Tips パラメータを外部から設定できるようにするためには何を使うべきか?

SerializeField 属性(Inspector)を使う

一番手っ取り早いのはサンプルコードAでも紹介している SerializeField 属性を使ってInspectorから設定できるようにする対応です

これは複数パターンを試したり色んなところで使い回したりしないようなパラメータ(ゲーム全体の設定など)を弄れるようにしたい場合に使いましょう

ScriptableObject を使う

SerializeField を使う場合に比べて実装コストは少し上がりますが、サンプルコードBのように複数パターンを試したり色んなところで使い回したりするような場合に効果を発揮できます

一度構造を作ってしまえばデータを複製してパターンも作れますし、そのままAssetBundle化することも可能です

のちのちエンジニアの手を使わずにデータのチューニングをしていくことを見越しておくと先にデータを外部ファイルに逃がしておいたほうがよいです

JSONを使う

利点は ScriptableObject と同じです

最終的にそのデータをどのように取得してくるかを想定して使い分けるとよいと思います

APIを叩いてレスポンスを受け取ってから次の処理に進むようなケースが想定されている場合はデータをJSONで定義しておいたほうが都合が良いでしょう(ステージの難易度情報やデッキの情報など)

「別の機能で使っている演出をとりあえず使い回してほしい」

仮で用意しておき、後から違うものに入れ替えるという状況もプロトタイプ開発ではよくあるケースです

プロトタイプ開発ではゲームの中身をどれだけ面白くできるかに注力して進めていくので、見た目の作り込みが後追いになることが多いです

進め方によってはデザイナーは関与せずにプリミティブなオブジェクト(簡単な記号や図形)のみを組み合わせただけでプロトタイプ開発を行うこともあります

そういった場合でも手戻り少なく実装を進めるにはどうするべきか、こちらもコードを見てみましょう

サンプルコードA

class Player : MonoBehaviour
{
    // HPを回復したときに呼ばれる
    void OnHeal()
    {
        // 回復演出は出しっぱなしにしてそのまま消す
        var effect = Instantiate(healEffectPrefab, transform);
        Destroy(effect, 1.5f);
    }

    // HPがゼロになったときに呼ばれる
    void OnDead()
    {
        if (isDead)
        {
            return;
        }
        StartCoroutine(PlayDeadEffectCoroutine);
    }

    IEnumerator PlayDeadEffectCoroutine()
    {
        var effect = Instantiate(deadEffectPrefab, transform);
        yield return new WaitForSecond(2.0f);

        Destroy(effect);
        // ゲームオーバー処理
        GameOver();
    }
}

サンプルコードB

class Player : MonoBehaviour
{
    // HPがゼロになって死亡演出が完了した後に呼ばれる
    public Action OnFinishDeadEffect { get; set; }

    // HPを回復したときに呼ばれる
    void OnHeal()
    {
        EffectManager.EmitHealEffect();
    }

    // HPがゼロになったときに呼ばれる
    void OnDead()
    {
        EffectManager.EmitDeadEffect(OnFinishDeadEffect);
    }
}

サンプルコードAでは Player クラスに直接エフェクトの発生処理を書いているのに対し、サンプルコードBでは エフェクトの発生処理を EffectManager にお願いする形になっています

自機のための演出になるので Player クラスに処理を書くと言う考え方は間違ってはいませんが、ゲームでの演出処理は「演出が終わったら」や「途中で中断したい」などの複雑な対応をするケースが多いです(そして調整の度にそのタイミングが代わります)

Prefabなどにまとめて全て演出側に処理を逃がせれば理想的ですが、内容がコロコロ入れ替わるプロト開発ではいきなり全ての処理を外に逃がすのも難しいです

そこでサンプルコードBでは EffectManager なる演出の複雑な遷移などをアレコレ吸収してくれる機能を外に用意しました

このようにしておくことで演出の差し替えが後で入ったとしても基本的には EffectManager に手を入れるだけにすることができます

ただし EffectManager をそのままエフェクトを何でも処理してくれる便利屋として扱っていくと今度は EffectManager が肥大化していって整理が難しくなってきてしまうので、その場合は様子を見つつ EffectManager も分割していきましょう

まとめ

ということでここまでつらつらと書き連ねて来ましたが、内容的にはプログラミングを設計する上で気をつけた方がよい基本的な事柄をまとめているということに気づきましたでしょうか

要は プロトタイプ開発であろうと速度重視の書き捨てコードを書くべきではない というのが私の結論です

少人数(1人)で開発を進めていくとついついルーズにコードを書いてしまいがちですが、開発が進んでいけば自分以外の人も多く関わってくることを考えると常に整理しながら開発を進めていけるのが理想です

個人の開発だったとしても、何日か経ったあと自分の書いたコードを見返して何が書いてあるのかよく分からなくなると言う経験はしたことがあるはずです

未来のチームメンバー(自分)が少しでもラクにできるコードを書くように心がけていきましょう!

明日の19日目はsuzu-jさんです。よろしくお願いします。

〜エンジニア不要で納品をまわす仕組み〜

このエントリーは、KLab Advent Calendar 2016 の12/24の記事です。

KLabのとある新規アプリ開発プロジェクトでは、アプリ内で使用する制作物の納品フローを工夫して、エンジニアの手がどんどん不要になってきています。

現在稼働しているワークフローを紹介します。

納品フローに関わるプロジェクトチームは大きく5つの班に別れています。

  1. ゲーム内の各種パラメータを入力する「企画班」
  2. UIパーツを作成する「UI班」
  3. 演出を作成する「演出班」
  4. サウンドを作成する「サウンド班」
  5. プログラム作成の全般を担う「開発班」

以上の各班の作業内容がプロジェクトの最終成果物としてのipaやapkファイルに取り込まれるまでの流れです。

以下では単に「バイナリ」と書いた場合ipa、apkファイルのことを指します。

また、プロジェクトで使用しているGitレポジトリは

  • client-server ソースコード・パラメータTSV用
  • ui-effects 演出・UIアセット用
  • sound サウンド用

と分割されています(実際には細かいものがもっとあります)。

各レポジトリのメインブランチは、origin/masterを使用しており、バイナリ作成時に最終的に使われるブランチです。

そのため、いつでもバイナリ作成ができるようにメインブランチは常に正しく動作することが求められます。

workflow

ポイントは2つです。

  1. エンジニアの手を介さずにバイナリをビルドし動作確認ができる
  2. エンジニアの手を介さずにメインブランチが更新できる

1. エンジニアがいなくてもバイナリをビルドし動作確認ができる

1番目のポイントは、誰でもSlack上でBotに話しかけることで簡単にバイナリ作成が出来るので最新のメインブランチに自分の作業内容を取り込んでデバッグ用端末上ですぐに動作確認ができることです。

話しかける内容はこんな感じです。

@builder1.1
os=ios
branch=origin/master
ui-effects=origin/update-effect1
sound=origin/master
title=演出1更新確認
description=個人テスト用_演出1更新

自分が動作確認したいブランチとOSを指定してタイトル、説明文を書くだけです。

初めての人でも3分教えれば操作を覚えられます。

これが導入される前は、ビルドは週末のプロジェクトの正式ビルド時のみで、開発班以外にとって自分の作業内容がデバッグ用端末上で確認できるまで数日待つ必要がありました。

それが今では10分ほどに短縮されました。

また、メインブランチの状態が動作確認されるのも数日に1回程度だったので、メインブランチが正常に動作しない時にどこでバグが入りこんだのか調査するのに時間がかかっていました。

今では、30分に1回は誰かがビルドしているので、メインブランチに何かあればすぐに開発班以外からバグ報告が来るのでバグの発見速度があがっています。

さらに、Jenkins上のジョブは触っていけない項目も多く、開発以外には解放していないプロジェクトも多いかと思います。

そんな場合でも、SlackのBotでラップしてしまえば必要な項目だけをパラメータ化でき、自由度の高いバリデーションもかけられるので、安全に実行してもらうことができます。

仕組みの構築は1時間程度で出来るものなので、メリットが大きいです。

実装は簡単で、SlackのReal Time Messagin API を使用してBotを作成し社内のサーバーでデーモンとして起動しています。

このデーモンが、Slackからのメッセージを受取り、ビルドパラメータにパースして、社内のJenkinsビルドサーバーに対してパラメータとともにPOSTリクエストを投げます。

ビルドが完了するとEMランチャにアップロードされ、Slackチャンネルに通知が来る、という流れです。

2. エンジニアを介さずにメインブランチが更新できる

2つ目のポイントは、開発班以外でもエンジニアの手を介さずにメインブランチの更新ができることです。

※もちろん一部必要な箇所は手動で取り込みます。

先に記した通りメインブランチは常に正常に動作することが求められているので、不正な変更は取り除く必要があります。

それにもかかわらずなぜエンジニアの手を介さずに更新ができるのかを説明していきます。

開発班以外がメインブランチを更新する流れには大きく2つ存在しています。

  1. UI・サウンド班はSVNを更新し、cronジョブがメインブランチへ自動定期取り込み
  2. 企画・演出班はGitレポジトリのメインブランチに対してプルリクを投げる

2.1. UI・サウンド班

UI班が納品するのは主にテクスチャで、サウンド班はオーサリングツールで書き出したサウンドデータを納品します。

この2班はもともとSVNに慣れていてGitに移行するメリットがなかったのでそのままSVNを使ってもらっています。

cronジョブが必要に応じた頻度で走っていて、SVN側で更新があると納品物に対する自動テストが走り、テストにパスするとGitレポジトリの方にコミットされて取り込まれます。

そのため、Gitレポジトリの方で開発班がデータに触る段階ではすでに命名規則などの自動テストの項目をすべて満たした状態が保証されています。

UI・サウンド班がSVNにデータをコミットするだけで、開発が手をかけずともメインブランチが更新できてしまいます。

UIは図3のフローです。

サウンドに関しては少し特殊で、目で見て分からない分動作確認の必要性が高いので、後述の企画・演出班と同様にUnityとGitの環境を構築しています。

サウンドは図1のフローです。

以下の流れになっています。

  1. サウンドデータをSVNにコミット
  2. soundレポジトリのorigin/stgにcronジョブで取り込まれる
  3. ローカル開発環境にorigin/stgをチェックアウトしUnityEditorで再生し確認
  4. Slackからorigin/stgを指定してバイナリビルド
  5. デバッグ用端末上で動作確認
  6. origin/stgをメインブランチにマージ

バイナリファイルや、大量のファイルは人間の目で確認するよりも機械的にチェックしたほうが不正なデータが取り込まれる可能性が減らせます。

2.2. 企画・演出班

図2のフローです。

企画班が納品するのはゲーム内の各種パラメータが記述されたTSVで、演出班は演出のプレファブとそれが使用するすべてのデータ (.prefab, .png, .mat, .anim, .controller, .fbx)を納品します。

企画班、演出班ともに開発班と同じフローで作業しています。

全員UnityとGitをインストールしており、各自の開発環境を構築しています。

サーバーは開発と同様に個人用サーバーにそれぞれ接続しています。

加えて、開発班以外は共通でGithubDesktopをインストールしています。

GithubDesktopを選んだ理由は主に3つです。

  • MacとWindowsに対応している。
  • Gitの複雑な概念を知らなくても使えるように複雑さを上手く隠蔽している。
  • 必要であればGitシェルを立ち上げて任意のGitコマンドが実行できる。

GithubDesktop独自の概念は例えば以下のようなものです。

  • 「Sync」- ローカルとリモートのブランチの内容をより新しいコミットの方に同期すること。
  • 「Update from master」- ローカルのmasterブランチの内容を作業ブランチにマージすること。
  • 「Publish」- 自分のローカルの作業ブランチをリモートにPushすること。

ブランチ切り替えや、PullReqもGithubDesktop上から出せます。

コンフリクトを発生させないために次のことに注意してもらっています。

  • 自分の作業内容をチームに周知し、同じファイルを編集しない
  • 作業開始前に必ずSyncで自分の環境を最新化する
  • 自動テストをパスしたPullReqはすぐにマージする
  • 自分のPullReqがマージされたら周知してそれぞれの環境でSyncさせる

また、Github EnterpriseのTeam機能を使って班ごとのTeamを作成し、アクセス権をコントロールしています。

例えば、演出Teamにはui-effectsレポジトリのみ読み書き権限を与え、その他のレポジトリは読み取りのみとしています。

これにより大雑把にレポジトリを守ることができます。

3. まとめ

納品フローに関する説明は以上になります。

この仕組みを作り上げるまでは当然開発の工数がそれなりに必要でした。

また、導入した当初は特にGit周りに関する質問が多く、開発班によるサポートが必要でした。

しかし今ではごくたまに発生するコンフリクト解消とシステム側のトラブル以外ではほとんどサポートが必要なくなってきています。

現時点での感想としては、

  1. 開発サイクルを速くまわす必要がある新規開発において早期の納品の仕組み化はおおきな価値があること
  2. 同じ作業を誰でもできるようにすることは想像を超える効果を生み出すこと

を感じています。

UI班から開発班へなどのファイルの受け渡しに関して当初は、プロジェクトの発足からしばらくは手渡しやファイル共有サーバーを使用していました。

そのため、単なる画像の差し替えやサウンドの差し替えにもいちいち開発班の手を介す必要がありました。

これは今振り返れば、開発サイクルを速くまわす必要がある新規開発においては時間的に大きなロスだったと感じます。

次の新規案件では最初の納品の前に仕組みを構築しておきたいものです。

Slackからのビルドシステムを作った当初の目的は、ビルドサーバーのJenkinsにアクセスしてパラメータを入力するのが面倒だったのでSlack上での会話の流れでビルドできたら素敵だよねというちょっとした遊び心でした。

それが今となっては各班が自分たちで作業と確認のサイクルをまわすためのコアとなっていることは驚きです。

繰り返し作業を単に自動化することと、それをさらに普段使い慣れたインターフェイスからすぐにアクセスできるようにすること、の間には大きな隔たりがあります。

前者は自分が作業を繰り返す回数を量的に変えるのに対し、後者は作業自体を質的に変える可能性をもっています。

今後もプロジェクトのみんなが幸せになる納品フローを常に模索していきたいです。

はじめに

オンラインゲームでは、金貨やコインといった ゲーム内で通貨のように利用可能なアイテム が必ずといって良いほど登場しますよね。
先日もこのTech Blogにて、ゲーム内で発行する仮想通貨のデータ分析業務についての記事が掲載されましたが、ゲーム内通貨の運用は各種法令等も絡むことからKLabでも特に注意して取り扱いを行っている業務の一つです。

そこで今日はゲーム内通貨の運用に関する業務やKLabとしての考え方を、会計上の観点からもう少し掘り下げてご紹介します。

法令におけるゲーム内の金貨等の位置づけ

オンラインゲーム内でアイテムの購入に利用できる金貨やコインといったトークン、上記ではゲーム内通貨と言いましたが、これは資金決済に関する法律(以下資金決済法、とします)で各種の規定がされています。そして上記のようなゲーム内の概念は、この法律の中では前払式支払手段という名前で定義付けがされています。

前払式支払手段の定義について条文にはちょっと難しく書いてあるのですが、できるだけウソにならない範囲で簡単に書くと以下のようになります。

  1. 発行の際にその金額・数量等が紙の証票(例としてチケットなど)またはデジタルデータとして記録されていること
  2. 1が対価と引き替えに利用者に対して発行されていること、つまり有償であること
  3. 1の内容と紐付く、IDや番号などが発行されていること
  4. 物品の購入やサービスを受ける際などに利用できるものであること

※ただし上記を満たす場合でも使用期限が6ヶ月以内のものは原則として対象外

上記はゲーム内の金貨やコインだけでなく、デパートなどで発行される商品券や交通機関などで使える回数券やプリペイドカード、英会話教室の前売りチケットなども上記の特徴を持つことが多く、つまりこれらは法律上同じ立て付けのものということになります。

ところで今年5月に改正資金決済法が成立し、bitcoinといった近年出てきた新しい決済手段に関する条文が追加されました。そこで このbitcoinのような新しい概念は、条文の中では 「仮想通貨」 という名称で定義づけされました。
「仮想通貨」というとどこかで聞いたような名前ですが、ここでいう仮想通貨とは前述の特定のサービスやゲーム内でのみ利用可能な前払式支払手段とは法律上別物でして、ここでは不特定の相手に対して利用可能なものを指します(正確にはそのほかにも仮想通貨を構成する要件があります)。従って法令改正後も前述の前払式支払手段に関する条文はほぼそのまま残っています。

なお冒頭でご紹介した記事のように、我々KLab内部ではゲーム内の金貨やコイン=前払式支払手段のことは 仮想通貨 と呼んでいるのですが、上記の法令改正の絡みもあり誤解を招くためこの記事の中では、法令に則り正確に 前払式支払手段 と呼ぶことにします。

オンラインゲームでの前払式支払手段の会計処理

当たり前の話ですが、会計において一番大事なことは売上を正確に勘定することです。
会計上、売上を計上するタイミングは物販業においては引渡基準と言って 原則的には役務提供(顧客に対してサービス提供や商品の引き渡しを行うこと)を完了した時点とすること になっています。
例えば自動車の販売を行った場合、売上を立てるのは契約日や入金日ではなく、この引渡基準に基づいて登録日あるいは実際に顧客に対して車の引き渡しを行った日とする運用にするのが一般的です。

この点でオンラインゲームにおける前払式支払手段は次のように若干特殊な性格を持ちます。
※ゲームにおける金貨やコインなどは無償でも配布することもありますが、ここでは有償で販売された物で無償配布のものは含まない(本来の意味での前払式支払手段)前提で説明します

オンラインゲームにおける前払式支払手段の流れ

上記で示すようにオンラインゲームにおける前払式支払手段とは、iOSのApp StoreやAndroidのGoogle Playストアのようなスマートフォンでの「決済プラットフォーム」によるアプリ内課金や、その他クレジットカード決済などで販売される「商品」です。しかし、顧客がその「商品」(例えばコインや金貨)を購入する動機は、これそのものにあるわけではなく、これと引き替えに何かアイテムを購入したい、とかサービスを受けたい、というさらに別の目的にあるわけです。
逆にコンテンツ提供者=”前払式支払手段の提供者”の視点からすると、前払式支払手段を販売しただけの段階では商品を引き渡す義務がまだ残っている状態だと考えることもできます。
つまり前払式支払手段は債権を表章した有価証券のようなものと考えると、前払式支払手段販売時点ではゲーム内における役務提供は完了していないため、この時点では売上計上は まだ できない、と考えるのがもっとも合理的なのです。最初にプリペイドカードや英会話前売りチケットのことを書きましたが、これと同じと考えればわかりやすいと思います。

これを会計的に整理して言うと、顧客が「決済プラットフォーム」で代金を支払った時点でまもなくコンテンツ提供者への入金は発生するもののこの時点では役務提供の終了(=売上発生)とは見做さず、 前受金処理(負債として計上すること)を行う 、ということになります。つまり顧客が実際に前払式支払手段を利用してアイテムを引き替えた時が前受金を取り崩すタイミング=すなわち売上として計上を行うタイミングになるわけです。

帳簿イメージ

この項の最初に書いた通り、会計において一番重要なことは売上を正確に計上することです。従って前払式支払手段を利用した業務においては、販売・発行時の情報と同じくらい、 消費した際の情報が大事 だということがおわかり頂けると思います。

前払式支払手段の残高管理と売上計上

多くのゲームにおいて前払式支払手段はまとめ売りの際に値段を下げることがあります。
例えば、金貨100枚だと120円(@1.2円)だが1,200枚まとめて買うと1,000円(@1.0円)になってお得!……といった販促方法は一般的にもよく見られるものですよね。

どの価格で購入した前払式支払手段であっても同様に利用できるという前提であれば、アイテム引き替えの実装だけを考えた場合、その前払式支払手段の総残高さえ分かればよいことになります。つまりそれぞれの単価を気にする必要は無いわけです。
一方で、上記で述べたように「売上を正確に計上する」という観点ではその実装だけでは問題が発生します。

  • それぞれ「単価いくらの前払式支払手段が消費されたか?」を記録しておかないと売上が正確に計上できない
  • 単一アプリにおける同一の前払式支払手段でも、Google PlayストアとKindleアプリのように購入元の「決済プラットフォーム」が混在することがある(売上は「決済プラットフォーム」毎にそれぞれ勘定するのが妥当であるため)

……ということがあるためです。そのため単に総残高を記録するだけでなく、前払式支払手段の種類別の 預入・払出のルールが厳密に運用 されていなければなりません。
このような要件は実は一般的な商品の在庫管理の考え方そのものでして、これに倣って前払式支払手段の残高を管理していくことになります。

在庫管理の基本的な方式は先入先出法、後入先出法、移動平均法といったものがありますが、「古いものから消費」というルールが顧客から見て最も自然でわかりやすいこと、預入・払出記録の1:1の突き合わせができるため詳細なトレース・分析が行いやすいなどの理由からKLabでは現状ほとんどのケースで先入先出法を採用しています。

さて、会計上一般的に在庫の管理を行う場合には 商品有高帳 という帳簿を作成します。前払式支払手段の内部管理においてもこれとほぼ同様のことを行うことになります。
例として、とあるゲームの金貨(=前払式支払手段)の預入・払出のサンプルを以下に示します。

金貨預入・払出サンプル

上記の例では、顧客であるユーザAは日を分けて金貨をそれぞれ100枚(単価 @1.2円)、1,200枚(単価 @1.0円)と購入し、その次の日にアイテムショップで500枚消費しています。ということで残高は (100+1,200)-500=800枚になる計算です。
肝心の内訳としては、先入先出で古いものから消費していくルールですから、単価1.2円の金貨全部と単価1.0円の金貨一部が取り崩され、後者が800枚だけ残ることになります。

このデータさえあれば売上を導出するのは簡単ですね。上記の「払出」に当たるログを全顧客分サマリーすれば良いわけです。
さらにここから前払式支払手段の残高も明らかなので、これをエビデンスとして資金決済法で定められた供託(後述します)を行うことができます。

その他資金決済法で定められた業務

これまで前払式支払手段の会計業務についてご説明してきましたが、KLabは資金決済法で定められる自家型の前払式支払手段発行者に該当しますので、上記の他にも法で定められた業務が存在します。
以下に簡単にご紹介します。

前払式支払手段の発行届出

資金決済法では基準日(毎年3月末・9月末)未使用残高が1,000万円を越える自家型発行者は、管轄の財務局長に前払式支払手段に関する情報(前払式支払手段の名称やその単価等)を書面で届出することが定められています。
KLabもこれに当たるため、関東財務局に前払式支払手段に関する届出を行っています。

発行保証金の供託

資金決済法では、保証金として発行済みの前払式支払手段の1/2以上の金銭を供託することが定められています。上述の発行届出と同様に、基準日残高でこれを計上します。
これは利用者保護のための運用で、万が一発行者が破綻してしまったような場合にはここから優先的に配当を行うことで、支払い済みの代金を丸損しないようにしているというわけです。

前払式支払手段の払戻し

銀行法や出資法という法律では、免許なしに事業として他人のお金を預かったり、その払戻しを行ったり、または送金をしたりすることを禁止しています(これらは社会的影響が大きな業務だからです)。
もし前払式支払手段がいつでも自由に払戻しができてしまうと、一旦それを購入後、好きなときに現金化ができるということになります。これは上述したお金の預入と実質同じことができることになってしまうため、発行済みの前払式支払手段を払戻しすることは資金決済法で禁止されているのです。この規定があるため、前払式支払手段の発行業者は滅多なことでは払戻しに応じることはありません。

一方で発行者が前払式支払手段を廃止した場合には、上記の例外として利用者に対して購入済みの前払式支払手段について払戻しの対応を行わなければならないことになっています。
利用者は発行者が設定した申し出期間(法令上最低60日間)内に申し出を行うことで払戻しが受けられることになります。
ちなみに、払戻しの期間を超えた場合は除斥(当該利用者を通常の払戻し手続きから除外して、相当する額の前払式支払手段残高を控除すること)することが可能になります。除斥された場合でも民法上の債権が消滅するわけではありませんが、この期間を過ぎてしまうとお金の回収が面倒になってしまいますので、未使用分の前払式支払手段があるサービスが終了してしまった場合は、事業者の告知を見たらぜひ早めに手続きをしてくださいね。

おわりに

アイテム課金型オンラインゲームというジャンルは、比較的新しい業態であるため日々利用者を保護するための新たな法令やガイドラインが検討されています。一方で、バックエンドの業務は通常の商品の在庫管理や売上管理等と考え方に大きな差はありません。
みなさんが前払式支払手段に関する業務を行うことがありましたら、この記事を何かの参考にしていただければ幸いです。


Shimanuki

分析基盤チームの高田です。

私のチームでは、各種ゲームのデータを集積するデータ分析システムの開発・運用を行なっています。

この記事では、モバイルオンラインゲームのクリティカルなポイントのひとつである仮想通貨の管理について、開発・運用という立場から気をつけていることを紹介したいと思います。

仮想通貨の扱いは、最終的には、法律・会計上の要請から決まってくるものだったりします。このため、開発者が単独で決められることは決して多くないです。しかし、ゲームの運用チームと会計部門の間に入ってルール作りを進めていくのは、経験上開発チーム主導で行なう方がうまくいくように思います。

重要なのは、「さまざまなユースケースを先に想定し、ルールを決めさせる」「システムの都合と、各部門の要件を整理する」というあたりです。要するに、「こういうケースがありえるんですが、この場合会計部門としてはどういうデータが必要ですか?」「運用チームはこのデータが見れれば問題ないでしょうか?」というのをどんどん聞いていくことが一番重要な仕事になります。

注意事項:

  • この記事は開発者としての立場で書かれたものであり、仮想通貨に関する法律上の判断については責任を負いかねますのでご注意ください。

仮想通貨とは?

ここで仮想通貨と呼ぶのは、(1)ユーザーが対価を支払って購入し、(2)アイテムとの交換、行動力回復、ガチャなどに使用できるもののことです。

「仮想通貨」と呼ぶとあまりなじみがないかもしれませんが、モバイルオンラインゲームでは、特別なアイテム(「石」「ジェム」など)を販売し、ユーザーはそれらを使用することで体力回復やガチャなどの機能を使用するという仕組みをとるものが多いです。これらのアイテムも仮想通貨と呼ばれます。なお、この記事では、iOS、Googleのネイティブアプリの事例を想定しています。

日本では、利用者保護の観点から、仮想通貨の扱いは資金決済法という法律で厳しく制限されています。また、モバイルオンラインゲームの売上は仮想通貨の発行時ではなく、消費時に計上することが多いため、仮想通貨の発行・消費ログは会計上も重要な数値となります。

恐怖の仮想通貨

KLabの場合、各ゲームのログを、共通のデータ分析システムに集約し、そこから会計部門、運用チーム、分析者などに各種データを提供するという形をとっています(図参照)。仮想通貨に関するデータも、このデータ分析システムに集約されます。私のチームが担当しているのは、このデータ分析システムの開発・運用の部分です。

virtual-coin

仮想通貨の管理は、以下のような理由で運用難易度の高いシステムになりがちです。

  1. 会計などに直結するクリティカルな部分であり、データの間違いなどが致命的な問題になりえる。
  2. ゲームごとに仕様やデータの格納方法もかなり異なってくる。
  3. 会計部門、運用チーム、データ分析に関わる部署など、関係者が多く、それぞれの要件も複雑である。

詳細はあとで紹介しますが、細かい要件がたくさんあり、「こんなに考えることがたくさんあると思わなかった」というくらい複雑になっていきます。

また、多くの関係者が存在するため、うまく調整ができなければ悲惨な事故にもつながりかねません。発生しうる事故の具体例をあげてみましょう。取りまとめるチームからすると、悪夢のような状況です。

  • 会計部門の要件がゲームの開発チームに伝わっていなかった。必要なログがデータベースにも残っておらず管理上必要なデータを用意できない。
  • 大量のデバッグ用ポイントを発行してしまって、本来記録すべき仮想通貨の発行と区別できない。
  • 新しい販売アイテムが法務や会計部門のチェックなしに追加されてしまった。本来仮想通貨として管理すべきものだが、管理に必要な機能が実装されていない。

こうした事故を防ぐためには、各方面からの要望を整理し、ルールを決めて運用していくことが重要です。

リリース時のチェックリスト

仮想通貨の仕様は各ゲームごとにかなり異なります。新規ゲームのリリース時などによく問題になるポイントを以下にあげてみます。リリース前にこの種のチェックポイントを確認していき、とにかく考慮漏れや想定外の事態を発生させないことが重要です。開発段階で関係者を集めてレビューすることが望ましいでしょう。

  • (1)仮想通貨を無償で付与することはあるか?
  • (2)どの仮想通貨から消費するか?
  • (3)通貨単位はどうなっているか?
  • (4)仮想通貨の種類は何種類あるか?
  • (5)引き継ぎ時のプラットフォームの扱い
  • (6)時間の扱い
  • (7)補填・デバッグの扱い

(1)仮想通貨を無償で付与することはあるか?

多くのゲームでは、仮想通貨相当のアイテム(「石」「ジェム」)を無償でも付与します。こうした事例では、有償で発行した仮想通貨と無償で発行したものを区別し、仮想通貨として管理するのは有償発行のものに限定するといったポリシーを採用する場合も多いでしょう。会計上売上として計上されるのは、有償発行のものに限定されますし、本来仮想通貨として管理すべきものは、ユーザーが対価を支払って購入したものに限定されるからです。

(2)どの仮想通貨から消費するか?

無償発行のものと有償発行の仮想通貨の両方がある場合、消費される順序をルールとして定めなければなりません。代表的なものは、以下の3パターンでしょう。ゲーム内でどのパターンを利用するのかは関係者間でよく確認しておきましょう。

  1. 有償発行分から消費する
  2. 無償発行分から消費する
  3. 有償無償問わず、先に発行した分から消費する。

(3)通貨単位はどうなっているか?

海外向けに提供している場合など、価格は日本円で統一されているとはかぎりません。また、iOSの場合、価格の設定にはApple Tierという独自の単位が使用されます。いずれにしても、どのような単位を使用して集計するのか、各チームで合意しておかなければなりません。

(4)仮想通貨の種類は何種類あるか?

仮想通貨は一種類とはかぎりません。ガチャチケットなどを販売する場合、そちらも仮想通貨として扱わなければならない可能性もあります。

(5)引き継ぎ時のプラットフォームの扱い

iOS端末からAndroid端末に変更した際など、異なるプラットフォーム間でユーザーデータを引き継ぐことがあります。引き継ぎに関しては、ゲームのシステム上のルールとして、仮想通貨も引き継ぐことを認める場合と、引き継ぎを認めない場合があります。仮想通貨の引き継ぎを許容する場合、消費された仮想通貨がどのプラットフォームで発行されたものかを取得する必要が生じるかもしれません。

(6)時間の扱い

時間の定義も問題になることがあります。海外向けに提供しているゲームの場合、タイムゾーンに気をつけなければならないのはもちろんですし、国内限定でも、取引日時を「ユーザーが端末上で操作した日時」とするのか「サーバー上でDBにレコードが作成された日時」とするのかが問題になることがあります。端末上の時間に合わせていると、過去の取引データが遅れて送信されてくることがあるからです。

例えば以下のようなケースを考えてみてください。

  1. 10/01 23:00 - ユーザーが仮想通貨を購入。通信エラーのため、データの送信がしばらく延期される。
  2. 10/02 01:00 - 仮想通貨集計バッチが10/01のデータを集計。
  3. 10/02 02:00 - 1のデータが再送されてくる。ところが10/01のデータはすでに集計済みである。

ここで再送されたデータを10/01のログとして集計しようとすると、集計から漏れてしまいます。

(7)補填・デバッグの扱い

障害時などに、通常の購入処理とは異なる形でユーザーに仮想通貨を付与することがあります。また運用中、購入処理などのデバッグが必要になる場面も想定されます。こうしたユースケースでは、デバッグのためにテスト用の仮想通貨を発行することがありえます。

こうしたイレギュラーなデータがログから漏れないよう、あらかじめ付与方法などについてもルールを決めておくことが必要です。

集計するログの種類

以下、KLabで仮想通貨管理のために集計しているログを参考のためにあげてみます。データの詳細はゲームごとの仕様によっても異なりますが、原則的には全ゲームで統一のフォーマットを使用して集計しています。

A. 仮想通貨発行ログ

仮想通貨の発行ログです。発行した個数や販売した仮想通貨商品(仮想通貨セット)のIDなどを集計します。モバイルゲームでは、App Store、Google Playストアなどのプラットフォームを通じて、仮想通貨をセット販売するので、セット単位で集計することになります。

項目 詳細
集計するタイミング デイリー
集計内容 ユーザーID、発行額、価格、通貨(JPY/USDなど)、日時、仮想通貨商品ID、プラットフォーム、仮想通貨の種類

B. 用途別仮想通貨消費ログ

消費用途別の仮想通貨消費ログです。発行した仮想通貨が、いつ、何の目的で、いくつ消費されたのかを集計します。

項目 詳細
集計するタイミング デイリー
集計内容 ユーザーID、消費額、日時、仮想通貨の種類、消費用途

C. セット別仮想通貨消費ログ

用途別の消費ログと似ていますが、発行時の仮想通貨商品ID(仮想通貨セット)別に消費を追ったものです。なぜこれが必要かというと、仮想通貨1個あたりの金額がセットごとに異なる場合があるからです。多くの場合、まとめ買いの方が得になるように商品価格が設定されています。また、途中で値段の変更を行なった場合などにも、この種のログが必要になります。

項目 詳細
集計するタイミング デイリー
集計内容 ユーザーID、消費額、日時、仮想通貨の種類、仮想通貨商品ID、発行時プラットフォーム

D. 仮想通貨残高

ここまでのログが正確に集計できていれば、そこから未使用の残高を計算することもできるはずです。基本的には、残高は、発行額合計 - 消費額合計になるはずです。

KLabでは、チェックのために、月に1回の頻度で、未使用仮想通貨の残高と上記のログから計算した残高を付き合わせています。

項目 詳細
集計するタイミング マンスリー
集計内容 仮想通貨個数、日時、仮想通貨の種類、発行時プラットフォーム

まとめ

以上、モバイルオンラインゲームの仮想通貨管理に関して、KLabで気をつけていることを紹介してきました。仮想通貨の扱いは、健全なゲームの運営やユーザーの利益保護という観点からきわめて重要なポイントになってきます。参考になれば幸いです。

KG SDKについて

KG SDKでは、KLabのノウハウを活かし、仮想通貨管理ライブラリやKPIレポートへの対応も行なっています。KG SDKの概要についてはこちら を、お問い合わせにつきましてはこちら をご覧下さい。

はじめに

こんにちは
KLab Advent Calendar8日目の記事です。
KLabGames事業部エンジニアの@knsh14です。

みなさんはUnityで開発をする際にどのようにしてアプリのパフォーマンスを計測していますか?
今回はある案件で今までと違うメモリ監視の方法を使ってみたら、デバッグが捗った話を紹介しようと思います。

KLabgamesでは幾つかのゲームでUnityを用いて開発を行っています。
よりよい品質でユーザ様に遊んでいただくために、常にパフォーマンス・チューニングを行っています。
その中でも各シチュエーションでのメモリ使用量を計測し、改善することは低スペック端末などで動作させるために必須です。
ですが、Unity標準のProfilerでは適切にメモリ使用量を計測できない問題がありました。

今までの方法

これまでは以下の様なコードでメモリ使用量を取得して画面に表示していました。

uint totalUsed = Profiler.GetTotalAllocatedMemory();
uint totalSize = Profiler.GetTotalReservedMemory();

System.Text.StringBuilder text = new System.Text.StringBuilder();

text.Append((totalUsed / (1024f * 1024f)).ToString("0.0"));
text.Append("/");
text.Append((totalSize / (1024f * 1024f)).ToString("0.0"));
text.Append(" MB(");
text.Append((100f * totalUsed / totalSize).ToString("0"));
text.Append("%) ");

これをOnGUIで表示するなり、NGUIのラベルに出力するなりしていました。

シンプルですね。
ここで使っているメソッドを調べてみましょう。
Profiler.GetTotalAllocatedMemory()
Profiler.GetTotalReservedMemory()

公式のリファレンスにもなにも書いてない...
実際に動かした結果とプロファイラを見比べてみると
device-screenshot
unity-profiler-screenshot
instruments-screenshot

全ての計測値にかなり開きがありますね。これでは計測の目安とするには不十分です。

ネイティブプラグインを書いてみる

ではどうすればいいのでしょうか?

僕らが見たいメモリ使用量は各OSから見てUnityAppがどれほどのメモリを使っているかというものです。
なのでUnityから利用できるネイティブプラグインを書いてそこから各OSに問い合わせればいいわけです。

こうして書いたネイティブプラグインコードがこちらです。

iOSの場合

#import <Foundation/Foundation.h>
#import <mach/mach.h>

unsigned int getUsedMemorySize() {
    struct task_basic_info basic_info;
    mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
    kern_return_t status;

    status = task_info(current_task(), TASK_BASIC_INFO, (task_info_t)&basic_info, &t_info_count);

    if (status != KERN_SUCCESS)
    {
        NSLog(@"%s(): Error in task_info(): %s", __FUNCTION__, strerror(errno));
        return 0;
    }

    return (unsigned int)basic_info.resident_size;
}

このコードをAssets/Plugins/iOSに適切に配置して読み込むとiOSからUnityAppが使用している物理メモリ量を取得することができます。
ネイティブプラグインを読み込む方法は世の中にたくさんあるのでここでは割愛します。

task_info関数は公式リファレンスによると、現在のtaskに関する情報の配列を返すとあります。
第2引数のflavorにTASK_BASIC_INFOを指定すると第3引数に与えたtask_infoに以下のような構造体が入ります。

struct task_basic_info {
        integer_t       suspend_count;  /* suspend count for task */
        vm_size_t       virtual_size;   /* virtual memory size (bytes) */
        vm_size_t       resident_size;  /* resident memory size (bytes) */
        time_value_t    user_time;      /* total user run time for
                                           terminated threads */
        time_value_t    system_time;    /* total system run time for
                                           terminated threads */
    policy_t    policy;     /* default policy for new threads */
};

この中のresident_sizeがtaskが使っているメモリ量です。
iOSではtask = プロセス = アプリなのでこれでiOSから見たUnityアプリがどれ位メモリを食っているかを確認することができました。

Androidの場合

Androidの方ではtask_infoは使えませんが、代わりにAndroid自体に取得できる機能があったのでそちらを使いました。

import android.os.Debug;
import android.os.Process;
import android.app.ActivityManager;
import android.content.Context;

/**
 * @return 現在使用しているメモリ量(KB)
 */
private static long getUsedMemorySize() {
    final Context context = UnityPlayer.currentActivity.getApplication().getApplicationContext();
    final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    final int[] pids = new int[]{ Process.myPid() };
    final Debug.MemoryInfo[] memoryInfos = activityManager.getProcessMemoryInfo(pids);
    long sumMemories = 0;

    for (Debug.MemoryInfo mi : memoryInfos) {
        sumMemories += mi.getTotalPss();
    }

    return sumMemories;
}

Android公式リファレンスを見るとDebug.MemoryInfoがいろいろな情報を持っているのでこちらを利用します。
プロセス毎にメモリ使用量が取れるのでそれを合算して表示します。

改修後

両プラットフォームともネイティブコードを書いたところで実際にプロファイラと見比べてみましょう

iOS

device-screenshot
instruments-screenshot

Instrumentsから得られた物理メモリ使用量と画面に表示しているメモリ使用量が一致していますね!

Android

device-screenshot

プロファイラからは以下のようになりました

kamata% adb shell dumpsys meminfo | grep gopher
    50409 kB: com.example.gopher (pid XXXXX / activities)
               50409 kB: com.example.gopher (pid XXXXX / activities)

50409 / 1024 = 49.2MB

この通りメモリ使用量がプロファイラから見たものと一致していますね!
こうしてUnityApp上で各OSからみたメモリ使用量を可視化することができました。
これで、実際の端末上で気軽にメモリ使用量を計測することができますね!

最後に

いかがでしたか。

このように簡単なコードを一手間加えるだけでプロファイリングが格段に楽になりましたね。
時には標準Profiler以外の方法も試してみると、よりUnityと上手く付き合っていけるのでは無いでしょうか。
この記事を見てくださった方のメモリ監視が楽になることをサンタさんにお願いして終わりたいと思います。

なお、今回作成したサンプルで使われているGopherのモデルはGitHubにあるこちらのモデルを利用しました。
Gopherの原著作者はRenée Frenchさんです。

次回は@hnwさんの「Autotools三兄弟の末っ子libtoolとは何者なのか」です。
僕の記事より面白そうなので今から楽しみです!

↑このページのトップヘ