20210321のUnityに関する記事は8件です。

Unity初心者がゲームを作ってみる②

前回①の続きから。

ランダムな位置にネコ影を配置できるようになりました

コメントくださった方+有識者兼友人のE氏により、無限増殖しないようにすることができました。
【原因】
ShadowGeneratorをネコ影オブジェクトにそのままアタッチしていたことにより、生成し終わった後にもう一度生成処理が走ってしまっていたため。
【解決方法】
空のオブジェクトを用意してそちらにShadowGeneratorをアタッチすることでゲーム開始時に1度だけ生成することが可能となる。
image.png

ネコ影にウキを近づけたら「突っつくネコ影スプライト」へ変更する

ウキが近づいてきたらネコ影がウキを突っついているようなスプライトに変更させていきます。
ウキとネコ影の距離を判定する必要があるので、DistanceManagerを作成しました。

DistanceManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/*
 * ネコ影とウキの距離判定をするクラス
 */
public class DistanceManager : MonoBehaviour
{
    GameObject shadow_1;
    GameObject uki_1;

    [SerializeField]
    private GameObject shadow_2;

    /*
    * ネコ影とウキのオブジェクトをみつける
    */
    public void FindNekoUkiObject()
    {
        this.shadow_1 = GameObject.Find("neko_1(Clone)");
        this.uki_1 = GameObject.Find("uki_1");
    }

    /*
    * オブジェクト間の距離を判定する
    */
    public void JudgeDistance()
    {
        Vector3 shadowPosition = shadow_1.transform.position;
        Vector3 ukiPosition = uki_1.transform.position;

        float distance = Vector3.Distance(shadowPosition, ukiPosition);

        if (distance <= 1.0f)
        {
           // つつくネコ影(shadow_2)へスプライト変更
           shadow_1.GetComponent<SpriteRenderer>().sprite = shadow_2.GetComponent<SpriteRenderer>().sprite;
        }
        else
        {
            // なにもしない
        }
    }
}

JudgeDistance()はウキとネコ影の距離を常に測っていたいのでUkiController.csのUpdate()で呼ぶように追加しました。

UkiController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/*
 * ウキの動きを制御するクラス
 */
public class UkiController : MonoBehaviour
{
    GameObject distanceManager;

    void Start()
    {
        // DistanceManagerを設定する
        distanceManager = GameObject.Find("DistanceManager");
        distanceManager.GetComponent<DistanceManager>().FindNekoUkiObject();
    }

    void Update()
    {
        // 左クリックされた場合
        if (Input.GetMouseButton(0))
        {
            // ウキ→カーソル方向のベクトル取得
            Vector3 vector = Camera.main.ScreenToWorldPoint(Input.mousePosition) - this.transform.position;
            // 正規化(ベクトルの大きさが1になる)
            Vector3 vectorUki = vector.normalized;

            vectorUki = new Vector3(vectorUki.x, vectorUki.y, 0);
            // ベクトルの向きにウキを移動
            this.transform.position += vectorUki;
        }

        // DistanceManagerの距離判定を呼び出す
        distanceManager.GetComponent<DistanceManager>().JudgeDistance();
    }
}

これでウキにネコ影が近づくと突っつくネコ影スプライトが表示されるようになりました。
image.png

ランダムな秒数待機して「沈むウキスプライト」へ変更する

次に、ランダムな秒数待機しウキが沈むようなスプライトへ変更させます。
待機してから処理を行う方法については下記サイトを参考に作成しました。
http://tsujitaku50.hatenablog.com/entry/2017/01/11/204150

DistanceManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/*
 * ネコ影とウキの距離判定をするクラス
 */
public class DistanceManager : MonoBehaviour
{
    GameObject shadow_1;
    GameObject uki_1;

    [SerializeField]
    private GameObject shadow_2;
    [SerializeField]
    private GameObject uki_2;

    /*
    * ネコ影とウキのオブジェクトをみつける
    */
    public void FindNekoUkiObject()
    {
        this.shadow_1 = GameObject.Find("neko_1(Clone)");
        this.uki_1 = GameObject.Find("uki_1");
    }

    /*
    * オブジェクト間の距離を判定する
    */
    public void JudgeDistance()
    {
        Vector3 shadowPosition = shadow_1.transform.position;
        Vector3 ukiPosition = uki_1.transform.position;

        float distance = Vector3.Distance(shadowPosition, ukiPosition);

        Sprite shadow1Sprite = shadow_1.GetComponent<SpriteRenderer>().sprite;

        if (distance <= 1.0f)
        {
            // shadow_1のスプライトが変更されていなかった場合
            if (shadow1Sprite.Equals(shadow_1.GetComponent<SpriteRenderer>().sprite))
            {
                // スプライト変更処理呼び出し 
                StartCoroutine("changeSprite");
            }
        }
        else
        {
            // なにもしない
        }
    }

    /* 
     * ネコ影とウキのスプライト変更を行う
     */
    IEnumerator changeSprite()
    {
        // つつくネコ影(shadow_2)へスプライト変更
        shadow_1.GetComponent<SpriteRenderer>().sprite = shadow_2.GetComponent<SpriteRenderer>().sprite;

        // ランダムな秒数待機する
        yield return new WaitForSeconds(Random.Range(4.5f, 7.5f));

        // 沈むウキ(uki_2)へスプライト変更
        uki_1.GetComponent<SpriteRenderer>().sprite = uki_2.GetComponent<SpriteRenderer>().sprite;

    }
}

JudgeDistance()を呼び出しているのがUkiControllerのUpdate()なので、changeSprite()が何度も呼ばれないように「shadow_1のスプライトが変更されていなかった場合」で処理の呼び出し判定を行っています。
以上の処理で、
ネコ影にウキを近づける→ウキを突っつくネコ影スプライトが表示
→何秒か待つ→沈むウキのスプライトが表示
ができあがりました。
image.png

次はユーザーの釣り上げ処理を作っていきます。
コードの改善点などありましたら是非お気軽にコメントください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PlayFabのGoogle Play Game連携でAddOauthScope("profile")が必要な件について

概要

Unity で Google Play ゲームを使用して PlayFab 認証を設定する(マイクロソフト公式, 2018/06/11)(以下、本家)
「Unity ゲームへの Google サインインの追加」のコードでAddOauthScope("profile")が必要になっていることの調査。

環境

本家ドキュメントなどを参考にひととおりセットアップが終わっているものとします。

Google サインインのコード

本家ドキュメントからコピペ

PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
.AddOauthScope("profile")
.RequestServerAuthCode(false)
.Build();
PlayGamesPlatform.InitializeInstance(config);

サインインのダイアログ

「Googleで公開されているお客様の個人情報とお客様を関連付ける」とかいう、ちょっとものものしく感じる許可を求められます。
言っても公開されている個人情報ってデフォルトは氏名ぐらいではあるんですが、ゲーム程度で要求されたらサインインを躊躇orプレイ自体離脱するユーザーもいそうな気配。
image.png

PlayFabマスタープレイヤーアカウント

サインイン→PlayFabClientAPI.LoginWithGoogleAccountしたら、作成されたPlayFabのマスタープレイヤーアカウントにもガッツリ氏名が出てきます(下側の黒塗り部分)
image.png
アカウント連携の目的って、端末変更した時のユーザー紐付けだけできたら良くて、氏名はゲームのアカウントに必要な情報ではないと思うんですけどねー

AddOauthScope("profile")を削除してみる

PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
.RequestServerAuthCode(false)
.Build();
PlayGamesPlatform.InitializeInstance(config);

必要なのはPlayFabClientAPI.LoginWithGoogleAccountのrequestに含めるServerAuthCodeだけで、profileとか要らないのでは? と思ったんですが、
AddOauthScope("profile")無しだと、PlayFabClientAPI.LoginWithGoogleAccountで以下のエラーが返ってきて、PlayFabログインに失敗します。
(PlayFabError.GenerateErrorReportをDebug.Logしたものをlogcatで取得)

I Unity   : /Client/LoginWithGoogleAccount: GoogleOAuthNoIdTokenIncludedInResponse

サインインのダイアログ

「Googleで公開されているお客様の個人情報とお客様を関連付ける」がなくて、健全っぽい。
(でもPlayFabログインでエラーになるから、ダメ)
image.png

Firebaseなど他のWebサービスとの連携のサンプルコードをWebで見ていると、このAddOauthScope("profile")無しパターンなので、PlayFabもそれじゃダメなのかなーと思うわけですが、うーむ :thinking:

RequestIdToken()してみる

GoogleOAuthNoIdTokenIncludedInResponseというエラーコードをふまえて、IdTokenが要るのかな? と思ってAddOauthScope("profile")の代わりにRequestIdToken()を追加してみました。

PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
.RequestServerAuthCode(false)
.RequestIdToken()
.Build();
PlayGamesPlatform.InitializeInstance(config);

これだとサインイン→PlayFabClientAPI.LoginWithGoogleAccountでPlayFabアカウント作成できました。

サインインのダイアログ

「Googleで公開されているお客様の個人情報とお客様を関連付ける」有り :sob:
image.png

APIのスコープ説明を見てると、OAuthでopenid取り扱うこと自体がこれにあたるから、この個人情報〜の許可確認はあって然るべきということなのかな?
image.png

PlayFabマスタープレイヤーアカウント

氏名は取得されなくなったので、ちょっとだけ健全。
image.png

ここはユーザーに見える画面ではないので、ユーザー目線では何も変わっていないけど、現状はこれが落とし所なのかなーという感じです。

宣伝

そんなこんなでGoogle Play ゲームとPlayFabを連携させたゲームがこちらです。

PlayFabでは、先に作ったWebGL版(unityroomで公開)と共有になるランキング(リーダーボード)機能程度しか使っていないのに、個人情報の関連付け要求とか表示されるのしのびないのですが…
Google Play ゲームのサインインを拒否した場合も、Android端末に紐づけてユーザー作るので、遊べないことはないです。
あとGoogle Play ゲーム初めて使ったついでに、実績(クエスト、アチーブメント)もつけてみました。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【M1 BigSur】JenkinsからGit(LFS)のチェックアウトからUnityへのビルドまで(Github)

概要

Mac Book Air (M1 2020)
macOS Big Sur 11.2.2

【M1 BigSur】Jenkins導入編
こちらで書かせたいただいた記事の続きです。
この記事を残しておきたかったのでついでにJenkins導入を書いた次第です。
JenkinsからGitでチェックアウトができればいいので別にUnityでなくてもいいですが今回は確認のために利用しています。

一連の流れを記事にしましたが割と困るのが
BigSurでのgit-lfs filter-process: git-lfs: command not foundエラーの回避方法だと思います。

準備

Github

Githubで作成したリポジトリーを使っているのでアカウントは作成しておきましょう。

unityのインストール

・とりあえずUnityはインストールしておいてください。
 今回はUnity2020.3.0f1で試しています。

GitのSSHキー(作っていない場合)

$ ssh-keygen

いろいろ聞かれるみたいですがすべて空白ままエンターして続けました。

生成場所は
Users/[ユーザー名]/.ssh/
に作られるのでこちらを使用してGithubからSSHを登録します。

image.png

GithubのアカウントからSettings>SSH and GPC keysを選択してNew SSH Keyを選択する。

image.png
設定 内容
Title これは好きに入力してください。
Key ここは先ほど作成したUsers/[ユーザー名]/.ssh/id_rsa.pubの中をコピーして貼り付けてください。

このように作られます。

image.png

JenkinsのUnityプラグインを導入

image.png
image.png

インストール後に再起動してもらうためにDownload now and install after restartを選択しました。

image.png

インストールできました。

image.png
image.png

設定したら保存しましょう。

インストールディレクトリにはjenkinsをインストールしたUnityのパスを設定しますが
FinderでUnityからoptionを押しながらするとパスをコピーできます。

unitypath.png

ジョブを作成

image.png

最初なので

unitypath.png

ジョブ名:unity_testお好きな名前
フリースタイル・プロジェクトのビルド
で作成します。

unitypath.png

認証情報を追加します。

unitypath.png

追加した認証情報を選択して保存します。

unitypath.png

これでプロジェクトの設定はできました。

Unityのビルドをしてみる

リポジトリーの準備ができたのでunityに対してビルドしてみます。

適当にビルドするものを用意して試してみました。

BuildTest.cs(ビルドの確認)
BuildTest.cs
using UnityEngine;

namespace Build
{
    /// <summary>
    /// ビルドのサンプル
    /// </summary>
    public class BuildTest
    {
        public static void HelloWorld()
        {
            Debug.Log("Hello World!");
        }
    }
}

Invoke Unity3d Editorを選択して

image.png

image.png

ビルドの確認のために作ったビルドをjenkinsから呼び出してみます。

-quit -batchmode -projectPath /Users/[ユーザー名]/.jenkins/workspace/[githubのプロジェクトパス] -executeMethod Build.BuildTest.HelloWorld

/Users/[ユーザー名]/.jenkins/workspace/[githubのプロジェクトパス]

を設定してビルド実行してみましょう。

ビルド時のエラー

git-lfs filter-process: git-lfs: command not found

GitLFSをインストールしているか確認してなければインストール

ターミナル:-zsh
% git lfs version
インストールなし
git: 'lfs' is not a git command. See 'git --help'.
インストールあり
git-lfs/2.13.2 (GitHub; darwin amd64; go 1.15.6)

git-lfsをインストール

ターミナル:-zsh
% brew install git-lfs

BigSurではCatalinaと違って読み取り専用な/usr/bin/などを一時的に無効にしてシンボリックリンクを作成できないみたいなので、Appleの公式の書かれている書き込み可能なマウントパスに設定してgit-lfsのシンボリックリンクを作成します。

・FileVaultを無効にする
FileVault.png

・リカバリモードで再起動して実行します(M1の場合は電源ボタンを長押ししてリカバリーモードに入ります。)

セキュリティの確認をします。

ターミナル:-zsh
% csrutil status
System Integrity Protection status: enabled.

enabledの場合は無効に

ターミナル:-zsh
% csrutil disable
ターミナル:-zsh
% csrutil authenticated-root disable

MacOSに再起動します。

ここからgit-lfsのシンボリックリンクを作成するためにマウントフォルダーを作成してディスクをマウントします。

ターミナル:-zsh
% mkdir ~/mount
ターミナル:-zsh
% sudo mount -o nobrowse -t apfs /dev/[デバイス] ~/mount

[デバイス]はディスクユーティリティの値を見ます。

あなたのルートを検索-あなたのルートは/dev/disk1s2s3であればmount実行し、最後のを切り落とす、例えば、あなたは/dev/disk1s2をマウントします

Appleの公式に書かれているの通りにdisk3s1s1の場合はdisk3s1でマウントします。

diskdeveice.png

ターミナル:-zsh
% sudo ln -s /usr/local/bin/git-lfs ~/mount/usr/bin/
ターミナル:-zsh
% sudo bless --folder ~/mount/System/Library/CoreServices --bootefi --create-snapshot

そのまま再起動

ターミナル:-zsh
% sudo reboot

再度jenkinsのビルド実行してみてください。

参考リンク

【Jenkins】GitのSSH接続をするための認証情報を設定する【GitHub】
JenkinsでUnityプロジェクトをビルドする
Unityコマンドライン引数
「macOS Big Sur 」でgit-lfs filter-process: git-lfs: command not foundに対応する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub for Unityでコミットできない時

起こった現象

プロジェクトの途中でGitHub for Unityを導入したところ、Commitボタンを押しても「Staging Changes...」のまま動かず、しばらく経つとコミット自体が自動キャンセルされる。

(GitHub for Unityの導入方法は以下の記事を参考にさせていただきました。)

【超初心者向け】Unityのプロジェクトを、GitHub for Unityを使って超簡単にバックアップする方法 - Qiita

解決策

結局このプロジェクト内では何をやっても直らず、新しいプロジェクトを作って移植したらうまくいきました。

プロジェクトの最初にGitHub for Unityを導入するのがおすすめです。

途中から入れた場合でもコミットできるようになる方法があれば、コメント頂けると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

github actionsでunityビルドしてiOSとAndroidのストアに自動的に提出する

こんにちは。virapture株式会社もぐめっとです。

mogmet.jpg

この記事を書いてるときは冬なんですけど、もう自分の写真がなさすぎて夏までさかのぼってしまいました。

本日はCI/CD with Unity, GitHub Actions, and Fastlaneという記事を参考にUnityでの自動ビルドとストア提出を作ってみたのでそのメモ書きを残しておきます。

概要

UnityにはUnity Cloud Buildという公式のCIがあるのですが、いかんせんfastlaneとの連携ができないので、証明書を引っ張ってきたり、ストアにアップロードというのができません。

そこで探してみたところ、GameCIが出しているunity-builderというgithub actionsを使ってビルドを行い、デプロイにはfastlaneを使用することでうまくできました。

このunity-builderが結構優秀で、ライセンスの認証とかもやってくれる。そして、ビルドが終わった後はライセンスをリターンしてくれるという優れもの。
もはやUnity Cloud Build使わなくてもCIができちゃいます。

構築手順

github actions workflowの設置

記事の人のリポジトリのファイルをベースにしながら作りました。

ios,android以外にもmac, windowsなどもあったのですが、邪魔だったのでそのへんは抹消しました。

こんなワークフローになってます。

image.png

iOSのビルド長すぎぃぃ。。
お待ちかね実体ファイルはこんな感じになってます。

name: Test, Build, and Release CGS

on:
  # push: { branches: [master] } #masterにpushされたときにやりたい人はこちらをお使いください
  release: { types: [published] }

env:
  UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
  UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
  UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
  BUILD_NUMBER: ${{ github.run_number }}

jobs:
  tests:
    name: Test Code Quality
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Cache Library
        uses: actions/cache@v2
        with:
          path: Library
          key: Library
      - name: Run EditMode and PlayMode Tests
        uses: game-ci/unity-test-runner@main
      - name: Publish Test Results
        if: ${{ always() }} # Avoid skipping on failed tests
        uses: davidmfinol/unity-test-publisher@main
        with:
          githubToken: ${{ secrets.GITHUB_TOKEN }}

  buildWithLinux:
    name: Build for ${{ matrix.targetPlatform }} by Unity
    runs-on: ubuntu-latest
    timeout-minutes: 90
    needs: tests
    strategy:
      fail-fast: false
      matrix:
        targetPlatform:
          - Android
          - iOS
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
          lfs: true
      - name: Cache Library
        uses: actions/cache@v2
        with:
          path: |
            Library
            build/${{ matrix.targetPlatform }}
          key: Library-${{ matrix.targetPlatform }}-
          restore-keys: Library-
      - name: Free Disk Space for Android
        if: matrix.targetPlatform == 'Android'
        run: |
          sudo swapoff -a
          sudo rm -f /swapfile
          sudo apt clean
          docker rmi $(docker image ls -aq)
          df -h
      - name: Build Unity Project
        uses: game-ci/unity-builder@main
        with:
          customParameters: -buildNumber ${{ github.run_number }}
          targetPlatform: ${{ matrix.targetPlatform }}
          buildMethod: Cgs.Editor.BuildCgs.BuildOptions
          androidAppBundle: true
          androidKeystoreName: keystore.keystore
          androidKeystoreBase64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
          androidKeystorePass: ${{ secrets.ANDROID_KEYSTORE_PASS }}
          androidKeyaliasName: ${{ secrets.ANDROID_KEYALIAS_NAME }}
          androidKeyaliasPass: ${{ secrets.ANDROID_KEYALIAS_PASS }}
      - name: Upload Build
        uses: actions/upload-artifact@v2
        if: github.event.ref != 'refs/heads/develop'
        with:
          name: cgs-${{ matrix.targetPlatform }}
          path: build/${{ matrix.targetPlatform }}

  releaseToGooglePlay:
    name: Release to the Google Play Store
    runs-on: ubuntu-latest
    timeout-minutes: 60
    needs: buildWithLinux
    if: github.event.action == 'published'
    env:
      GOOGLE_PLAY_KEY_FILE: ${{ secrets.GOOGLE_PLAY_KEY_FILE }}
      GOOGLE_PLAY_KEY_FILE_PATH: ${{ format('{0}/fastlane/api-finoldigital.json', github.workspace) }}
      ANDROID_BUILD_FILE_PATH: ${{ format('{0}/build/Android/Android.aab', github.workspace) }}
      ANDROID_PACKAGE_NAME: com.sample.app # 自分のアプリのパッケージ名に合わせよう!
      RELEASE_NOTES: ${{ github.event.release.body }}
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Download Android Artifact
        uses: actions/download-artifact@v2
        with:
          name: cgs-Android
          path: build/Android
      - name: Prepare for Upload
        run: |
          echo "$GOOGLE_PLAY_KEY_FILE" > $GOOGLE_PLAY_KEY_FILE_PATH
          echo "$RELEASE_NOTES" > fastlane/metadata/android/en-US/changelogs/default.txt
      - name: Upload to Google Play
        uses: maierj/fastlane-action@v1.4.0
        with:
          lane: 'android playstore'

  buildIOS:
    name: Build Archive for iOS
    runs-on: macos-latest
    timeout-minutes: 60
    needs: buildWithLinux
    if: github.event.action == 'published'
    env:
      APPLE_CONNECT_EMAIL: ${{ secrets.APPLE_CONNECT_EMAIL }}
      APPLE_DEVELOPER_EMAIL: ${{ secrets.APPLE_DEVELOPER_EMAIL }}
      APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
      APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
      APPLE_ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
      MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
      MATCH_PERSONAL_ACCESS_TOKEN: ${{ secrets.MATCH_PERSONAL_ACCESS_TOKEN }}
      IOS_APP_ID: com.sample.app # bundle idを指定します
      IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
      PROJECT_NAME: onenitejinro
      RELEASE_NOTES: ${{ github.event.release.body }}
      MATCH_REPOSITORY_ACCOUNT: ${{ secrets.MATCH_REPOSITORY_ACCOUNT }}
      USYM_UPLOAD_AUTH_TOKEN: 'fake' # ビルドが途中でこけるのでfake用に環境変数を追加。
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Cache restore for debug
        uses: actions/cache@v2
        with:
          path: |
            Library
            build/iOS
          key: Library-iOS-
          restore-keys: Library-
      - name: Download iOS Artifact
        uses: actions/download-artifact@v2
        with:
          name: cgs-iOS
          path: build/iOS
      - name: Cache restore cocoapods # firebase使ってるとcocoapodsを使うのですが、cocoapodsのキャッシュとらないと毎回時間かかるのでキャッシュしておきます。
        uses: actions/cache@v2
        if: ${{ always() }}
        with:
          path: |
            build/iOS/iOS/Pods
            ~/.cocoapods/repos
          key: Pods-${{ hashFiles('**/Podfile') }}
          restore-keys: Pods-
      - uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.7.2'
      - name: Prepare for fastlane # GateKeeper対策
        run: |
          sudo spctl --master-disable
      - name: Archive iOS
        uses: maierj/fastlane-action@v2.0.1
        with:
          lane: 'ios build'
      - name: run if fail_step failed # ビルドがコケた原因がわかるようにcatしておきます
        if: failure()
        run: cat /Users/runner/Library/Logs/gym/*Unity-iPhone.log
      - name: Upload Build
        uses: actions/upload-artifact@v2
        if: github.event.ref != 'refs/heads/develop'
        with:
          name: ipa
          path: |
            ${{ github.workspace }}/*.ipa
            ${{ github.workspace }}/*.dSYM.zip

  releaseToAppStore:
    name: Release to the App Store
    runs-on: macos-latest
    timeout-minutes: 60
    needs: buildIOS
    if: github.event.action == 'published'
    env:
      APPLE_CONNECT_EMAIL: ${{ secrets.APPLE_CONNECT_EMAIL }}
      APPLE_DEVELOPER_EMAIL: ${{ secrets.APPLE_DEVELOPER_EMAIL }}
      APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
      APPLE_TEAM_NAME: ${{ secrets.APPLE_TEAM_NAME }}
      APPLE_ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
      FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
      FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
      MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
      MATCH_PERSONAL_ACCESS_TOKEN: ${{ secrets.MATCH_PERSONAL_ACCESS_TOKEN }}
      ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
      ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
      ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
      IOS_APP_ID: com.sample.app # bundle idを指定します
      IOS_BUILD_PATH: ${{ format('{0}/build/iOS', github.workspace) }}
      PROJECT_NAME: onenitejinro
      RELEASE_NOTES: ${{ github.event.release.body }}
      MATCH_REPOSITORY_ACCOUNT: ${{ secrets.MATCH_REPOSITORY_ACCOUNT }}
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
      - name: Download iOS Artifact
        uses: actions/download-artifact@v2
        with:
          name: ipa
          path: |
            ${{ github.workspace }}/*.ipa
            ${{ github.workspace }}/*.dSYM.zip
      - name: Upload to the App Store
        uses: maierj/fastlane-action@v1.4.0
        with:
          lane: 'ios release'

このファイルのポイントを話しておくと、linuxでビルドが終わった後、fastlaneを使ってandroid/iosともにビルドもしくはアップロードを行っています。
iosは結構曲者で、UnityでCLIビルドが結構大変なので随所随所にビルドできるような仕組みを置いてます。

fastlaneの設置

fastlaneで使うファイルを参考ファイルをベースにカスタマイズした下記を設置しています。

keychain_name = "temporary_keychain"
keychain_password = SecureRandom.base64

platform :android do

  desc "Upload a new Android version to the Google Play Store"
  lane :playstore do
    upload_to_play_store(
      aab: "#{ENV['ANDROID_BUILD_FILE_PATH']}",
      track: 'internal',
      skip_upload_screenshots: true,
      skip_upload_images: true
    )
  end

end

platform :ios do

  desc "Push a new release build to the App Store"
  lane :release do
    api_key = app_store_connect_api_key(
      key_id: ENV['ASC_KEY_ID'], # your key id
      issuer_id: ENV['ASC_ISSUER_ID'], # your issuer id
      key_content: ENV['ASC_KEY_CONTENT'], # your secret key body
      # ex) key_content: '-----BEGIN PRIVATE KEY-----\nfoobar\n-----END PRIVATE KEY-----'
    )
    upload_to_app_store(
      api_key: api_key, # pass api_key
      force: true,
      skip_screenshots: true,
      skip_metadata: true
    )
  end

  desc "Submit a new Beta Build to Apple TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV['ASC_KEY_ID'], # your key id
      issuer_id: ENV['ASC_ISSUER_ID'], # your issuer id
      key_content: ENV['ASC_KEY_CONTENT'], # your secret key body
      # ex) key_content: '-----BEGIN PRIVATE KEY-----\nfoobar\n-----END PRIVATE KEY-----'
    )
    upload_to_testflight(
      api_key: api_key, # pass api_key
      skip_waiting_for_build_processing: true
    )
  end

  desc "Create .ipa"
  lane :build do
    cocoapods(podfile: "#{ENV['IOS_BUILD_PATH']}/iOS/Podfile")
    disable_automatic_code_signing(path: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj")
    certificates
    update_project_provisioning(
      xcodeproj: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcodeproj",
      target_filter: "Unity-iPhone",
      profile: ENV["sigh_#{ENV['IOS_APP_ID']}_appstore_profile-path"], # より動的にみるようにしています
      code_signing_identity: "Apple Distribution: #{ENV['APPLE_TEAM_NAME']} (#{ENV['APPLE_TEAM_ID']})"
    )
    gym(
      workspace: "#{ENV['IOS_BUILD_PATH']}/iOS/Unity-iPhone.xcworkspace",
      scheme: "Unity-iPhone",
      clean: true,
      #clean: false,
      skip_profile_detection: true,
      codesigning_identity: "Apple Distribution: #{ENV['APPLE_TEAM_NAME']} (#{ENV['APPLE_TEAM_ID']})",
      export_method: "app-store",
      export_options: {
        method: "app-store",
        provisioningProfiles: {
          ENV["IOS_APP_ID"] => "match AppStore #{ENV['IOS_APP_ID']}"
        }
      }
    )
  end

  desc "Synchronize certificates"
  lane :certificates do
    cleanup_keychain
    create_keychain(
      name: keychain_name,
      password: keychain_password,
      default_keychain: true,
      lock_when_sleeps: true,
      timeout: 3600,
      unlock: true
    )
    match(
      type: "appstore",
      readonly: true,
      keychain_name: keychain_name,
      keychain_password: keychain_password
    )
  end

  lane :cleanup_keychain do
    if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
      delete_keychain(name: keychain_name)
    end
  end

  after_all do
    if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
      delete_keychain(name: keychain_name)
    end
  end
end

元ファイルからcocoapodsのインストールや、api keyまわりの設定、provisioning profileの指定方法などが若干違います。

また、問題を切り分けやすくするためにビルドとapp storeにリリースする処理はわけたりしています。

参考元ファイルでは現在、apikey周りは証明書をおいていい感じにやるようにしているみたいです。

他にfastlaneに付随するファイルも置いていきます

  • Appfile
for_platform :android do
  package_name(ENV["ANDROID_PACKAGE_NAME"])
  json_key_file(ENV["GOOGLE_PLAY_KEY_FILE_PATH"])
end

for_platform :ios do
  app_identifier(ENV["IOS_APP_ID"])

  apple_dev_portal_id(ENV["APPLE_DEVELOPER_EMAIL"])  # Apple Developer Account
  itunes_connect_id(ENV["APPLE_CONNECT_EMAIL"])     # App Store Connect Account

  team_id(ENV["APPLE_TEAM_ID"]) # Developer Portal Team ID
  itc_team_id(ENV["APPLE_ITC_TEAM_ID"]) # App Store Connect Team ID
end
  • Deliverfile
submit_for_review false
automatic_release true
force true
skip_screenshots true
release_notes({
  'default' => ENV["RELEASE_NOTES"],
  'en-US' => ENV["RELEASE_NOTES"]
})
run_precheck_before_submit false

submission_information({
  add_id_info_uses_idfa: false,
  export_compliance_compliance_required: false,
  export_compliance_encryption_updated: false,
  export_compliance_app_type: nil,
  export_compliance_uses_encryption: false,
  export_compliance_is_exempt: false,
  export_compliance_contains_third_party_cryptography: false,
  export_compliance_contains_proprietary_cryptography: false,
  export_compliance_available_on_french_store: false
});
  • Matchfile
git_url("https://github.com/iosのcertificateおいてるgitリポジトリ") # sshのurlではなく、httpsで指定してます。
git_basic_authorization(Base64.strict_encode64("#{ENV['MATCH_REPOSITORY_ACCOUNT']}:#{ENV['MATCH_PERSONAL_ACCESS_TOKEN']}"))
storage_mode("git")
type("appstore") # The default type, can be: appstore, adhoc, enterprise or development

app_identifier(["com.sample.app"]) # bundleIDを指定しよう
username("apple@example.com") # Your Apple Developer Portal username

projectの準備

BuildCgs.csをAssets/Scripts/Cgs/Editorディレクトリに置いておく。
このファイルがビルド番号の設定などをしてくれている。

もぐめっとの場合、CIのビルド番号とアプリのビルド番号を紐付けしたかったので冒頭部分だけbuildNumberを参照するように少し修正しました。

修正後BuildCgs.cs
            PlayerSettings.macOS.buildNumber = options["buildNumber"];
            PlayerSettings.iOS.buildNumber = options["buildNumber"];
            PlayerSettings.Android.bundleVersionCode = int.Parse(options["buildNumber"]);
            PlayerSettings.WSA.packageVersion = new Version(options["buildVersion"]);

metadataの準備

metadataがないとfastlane途中でこけるため準備しておく。

androidの準備

fastlane run download_from_play_store json_key:<json path>

jsonファイルはストアにアクセスするために必要なものになるが、こちらの記事に解説は委ねます。

iosの準備

fastlane deliver download_metadata

上記で生成されたファイルをコミットしておく

githubのsecrets設定

先術したymlやfastfileなどを見てもらったとおり、環境変数をふんだんに使うので、その準備をしていきます。

game-ci/unity-builder周りで使う環境変数

細かいことはGame CIのドキュメントをご参照ください

Unityのアカウント周りの情報は下記の変数になります

  • UNITY_EMAIL
  • UNITY_PASSWORD
  • UNITY_SERIAL

unityのserialについてはunityのwebサイトにログインして確認します。
スクリーンショット 2021-03-21 15.40.06.png

Androidの公開設定周りはこちらの変数になります

  • ANDROID_KEYALIAS_NAME
  • ANDROID_KEYALIAS_PASS
  • ANDROID_KEYSTORE_BASE64
  • ANDROID_KEYSTORE_PASS

上記四点はandroidの公開設定でも使うこの辺の情報ですね。
image.png

keystoreの作り方は公式に委ねます

fastlaneのアップロード周りで使ってる環境変数

アップロードするときなどに使われるfastlaneのDeliverで使われるAppfileで定義して使ってる一覧です。

  • APPLE_CONNECT_EMAIL

App Store Connect Accountにアクセスできるアカウントを指定します

  • APPLE_DEVELOPER_EMAIL

Apple Developer Accountにアクセスできるアカウントを指定します。

  • APPLE_TEAM_ID
  • APPLE_TEAM_NAME

上記2点はDeveloper Portalのサイトでアクセスできる項目を確認します

スクリーンショット 2021-03-21 16.00.14.png

  • APPLE_ITC_TEAM_ID

APPLE_TEAM_IDではなくて、app store connectで使うアカウントのほうのIDになるらしい。とりあえずfastlane使えるならspaceship使えば取得できます。

  • ASC_ISSUER_ID
  • ASC_KEY_CONTENT
  • ASC_KEY_ID

Appleの2段階認証を突破してアップロードできるようにする用の変数です。解説は下記記事に委ねます。

  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
  • FASTLANE_PASSWORD

先程のAppStoreConnect API Keyを使った方法でできると思うので、おそらくこの2点は設定しなくてもいいと思いますが、念の為設定しておきます。

設定方法はこちらに。

  • GOOGLE_PLAY_KEY_FILE

metadataをダウンロードする時お話した、jsonファイルの中身をはっつけます。

再掲)

fastlane match周りの環境変数

fastlane matchについてはこちらをご参照ください。証明書を管理するやつです。

  • MATCH_PASSWORD

matchに設定してるパスワードを指定します。

  • MATCH_PERSONAL_ACCESS_TOKEN
  • MATCH_REPOSITORY_ACCOUNT

上記2つはMatchfileで指定したgit_basic_authorizationに使うものなのですが、repositoryのcloneがgithub action上でできるようにするために使っています。
MATCH_PERSONAL_ACCESS_TOKENはgithubのaccess tokenを指定します。

MATCH_REPOSITORY_ACCOUNTはMatchのリポジトリにアクセスできるアカウントを指定します。(access tokenを発行したアカウント名ですね)

使い方

ようやく設定がおわりました。

このworkflowはreleaseを切ったときに初めて発火されます。

リポジトリの右側にあるreleasesから
image.png

Draft a new releaseボタンからリリースを作りましょう!
image.png

成功すればreleaseで書いた内容でappstoreとplay storeにリリースされるはずです!

まとめ

というわけでgame ciさんのunity-builderを使うことでgithub actionsでunityのCIを回すことができるようになりました!
unityはビルドに時間かかるので結構めんどくさかったのですがこの辺が省略できるようになるのはとても楽になりました。

みなさんの開発にあてる時間がこれで増えたら幸いです!

最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!
他にもcameconoffchaといったサービスも作ってるのでよかったら使ってね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityを始める - オブジェクトの物理処理 -

はじめに

今までの学習で、Unityを触ったことがないという状態から、Unityで開発を始められるようになりました。
今回は、Unityでの開発で、「ピンボールゲーム」や「コイン落としゲーム」のような、物理演算を使うシンプルなゲームが作成できるようになる方法をまとめます。

Unityではrigidbodyを使えば、非常に簡単に物理演算を行うことができます。

過去の記事

Rigidbodyとは

Rigidbody を使うと、ゲームオブジェクトを物理特性によって制御する事ができるようになります。リジッドボディがフォースやトルクを受けると、オブジェクトはより現実的な動きをします。重力をうけたり、スクリプトを使ってフォースを与えたり、 NVIDIA PhysX 物理エンジンを通して他のオブジェクトの影響を受けるようにするためには、ゲームオブジェクトに Rigidbody を追加する必要があります。

参考サイト : https://docs.unity3d.com/ja/2020.2/Manual/class-Rigidbody.html

事前準備

  1. 新規でプロジェクト作成の作成を行います。
  2. Hierarchy に Cube を追加します。
  3. Hierarchy に Sphere を追加します。

実装

c5vle-743bs.gif

Sphere に Add Component で Rigidbody を追加します。

スクリーンショット 2021-03-21 14.06.10.png

実行すると Sphere が Cube へ落下します。

まとめ

Rigidbody を使う事で、ゲームオブジェクトに物理特性を加える事ができました。
Sphere が Cube へ落下後、跳ね返りには Physic Material を使用しています。

Physics の内容を把握する事で、よりリアルな表現が出来るようになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VContainer入門(1) - IContainerBuilderとIObjectResolver

この記事について

Unity用のDIコンテナであるVCointanerの使い方を解説します。DIコンテナを使うと複雑な依存関係を楽に管理できるようになります。

DI(DependencyInjection)そのものについて詳しい説明はここではしません。

環境

記事中のソースコードは以下の環境で試しています。

  • Unity 2020.3.0f1
  • VContainer 1.6.0

VContainerの導入

VContainerはUPM(UnityPackageManager)を通してプロジェクトにインポートできます。

まずはUnityで新しいプロジェクトを作成し、プロジェクトのPackagesフォルダにあるmanifest.jsonを開きます。
そしてdependencies内に以下の設定を追加します。

manifest.json
  "jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.6.0"

これでVContainerが使えるようになりました。

VCointanerを試してみる

動作確認用のクラスを作成

まずはサンプルとして使うための適当な依存関係があるクラスを作っておきます。

Mock.cs
using UnityEngine;
using VContainer;

// 先頭に[Logger]を付けてログ出力するクラス
public sealed class Logger
{
    public void Log(string message) => Debug.Log("[Logger] " + message);
}

// 足し算するだけのクラス
public sealed class Calculator
{
    public int Add(int a, int b) => a + b;
}

// LoggerとCalculatorに依存するクラス
public sealed class HogeClass
{
    private readonly Logger logger;
    private readonly Calculator calculator;

    [Inject]
    public HogeClass(Logger logger, Calculator calculator)
    {
        this.logger = logger;
        this.calculator = calculator;
    }

    public void LoggerTest()
    {
        logger.Log("LoggerTest");
    }

    public void CalculatorTest(int a, int b)
    {
        int result = calculator.Add(a, b);
        logger.Log($"{a} + {b} = {result}");
    }
}

HogeClassはコンストラクタでLoggerCalculatorを受け取ります。[Inject]属性が付いているのはVCointanerがこのコンストラクタを使えるようにするためです。1

VContainerを利用しない場合、HogeClassは以下のように使えます。

var logger = new Logger();
var calculator = new Calculator();

var hoge = new HogeClass(logger, calculator);
hoge.LoggerTest(); // [Logger] LoggerTest と出力される
hoge.CalculatorTest(3, 5); // [Logger] 3 + 5 = 8 と出力される

VContainerはHogeClassにLoggerとCalculatorを渡して生成する部分を肩代わりしてくれます。次の項から実際に使ってみます。

VContinerを使う流れ

VCointanerは以下の流れで使えます。

  1. IContainerBuilderを生成する。
  2. IContainerBuilderに使いたいクラスを登録する。
  3. IContainerBuilderからIObjectResolverを生成する。
  4. IObjectResolverを通して使いたいクラスを生成する。

実際にVContainerを使う際はLifetimeScopeを使うことが多いです。LifetimeScopeIContainerBuilderIObjectResolverをいい感じに管理してくれるクラスです。

このLifetimeScopeを通して使う場合でも内部の動作を知っていた方がわかりやすいので、まずはIContainerBuilderIObjectResolverを直接使ってみます。

実際のコードは以下になります。このコンポーネントを適当なGameObjectにアタッチして実行すると動作確認できます。

TestMonoBehaviour.cs
using UnityEngine;
using VContainer;

public sealed class TestMonoBehaviour : MonoBehaviour
{
    private void Start()
    {
        // 1. IContainerBuilderを生成
        IContainerBuilder containerBuilder = new ContainerBuilder();

        // 2. IContainerBuilderに使いたいクラスを登録
        containerBuilder.Register<Logger>(Lifetime.Singleton);
        containerBuilder.Register<Calculator>(Lifetime.Singleton);
        containerBuilder.Register<HogeClass>(Lifetime.Singleton);

        // 3. IContainerBuilderからIObjectResolverを生成
        using (IObjectResolver objectResolver = containerBuilder.Build())
        {
            // 4. IObjectResolverで使いたいクラスを生成
            HogeClass hoge = objectResolver.Resolve<HogeClass>();

            hoge.LoggerTest(); // [Logger] LoggerTest と出力される
            hoge.CalculatorTest(3, 5); // [Logger] 3 + 5 = 8 と出力される
        }
    }
}

コード中の1、2、3、4をそれぞれ説明していきます。

1. IContainerBuilderを生成

IContainerBuilderの実体としてContainerBuilderクラスを使います。普通にnewで生成します。

2. IContainerBuilderに使いたいクラスを登録 (Register)

IContainerBuilder.Registerを使ってクラスを登録できます。型引数に登録したいクラス、引数にLifetimeを渡します。

Lifetimeは生成されたオブジェクトの生存期間を指定するものです。あとで説明するのでとりあえずLifetime.Singletonを渡しておいてください。

3. IContainerBuilderからIObjectResolverを生成 (Build)

IContainerBuilder.BuildIObjectResolverを生成できます。IObjectResolverはDisposeが必要なのでusingで囲っています。

4. IObjectResolverで使いたいクラスを生成 (Resolve)

IObjectResolver.ResolveでBuildする前に登録しておいたクラスが生成できます。ここではHogeClassを生成しています。型引数に生成したいクラスを指定すればLoggerクラスやCalculatorクラスも生成できます。

Register/Build/Resolveの内部動作

VContainerの内部動作も知っていた方が便利なので簡単に説明しておきます。

Register

Registerした時点では登録されたクラスをContainerBuilderの内部に保存しているだけです。

Build

ContainerBuilderをBuildするとIObjectResolverが生成されます。
この段階で、Registerされたクラス全てをリフレクションで解析して生成に必要な情報を集めます。2

HogeClassの場合はInject属性がついたコンストラクタを発見し、引数にLoggerとCalculatorが必要なことが記録されます。
LoggerとCalculatorはInject属性がついたメンバがないのでただ生成すればいいことが記録されます。

Resolve

Build時に記録した情報に基づいて指定されたクラスのインスタンスを返します。

ここでResolveという単語は「依存関係を解決して生成されたオブジェクトを取り出すこと」を表します。

HogeClassの場合はLoggerとCalculatorが必要なので、まずはLoggerとCalculatorをResolveします。それからこのLoggerとCalculatorを使ってHogeClassのコンストラクタを呼び出してHogeClass自体を生成します。
要するにobjectResolver.Resolve<HogeClass>()の中ではnew HogeClass(new Logger(), new Calculator());と同様のことが実行されています。

依存先のクラス(ここではLoggerやCalculator)も再帰的にResolveされていくのが重要です。
例えば、LoggerがコンストラクタでFileWriterというクラスを受け取らなければならなくなったとします。この場合でもFileWriterがRegisterされていれば自動的にFileWriterがResolveされてLoggerのコンストラクタに渡され、そのLoggerがまたHogeClassに渡されます。依存関係が何段階になっても同様にResolveされていきます。3

Lifetimeについて

あとで説明すると言っていたLifetimeについて説明します。Lifetimeを変えるとResolve時の動作が少し変わります。

LifetimeにはLifetime.SingletonLifetime.TransientLifetime.Scopedの3つがあります。

Lifetime.Singleton

Lifetime.SingletonではResolveで生成されたインスタンスがIObjectResolver内でキャッシュされます。つまり複数回Resolveを呼んでも同じインスタンスが返ってきます。

using (IObjectResolver objectResolver = containerBuilder.Build())
{
    // hogeとhoge2は同じインスタンスになる
    HogeClass hoge = objectResolver.Resolve<HogeClass>();
    HogeClass hoge2 = objectResolver.Resolve<HogeClass>();
}

IObjectResolverごとにキャッシュされるためIObjectResolverが異なると別のインスタンスになります。
次の例では2回BuildしてIObjectResolverを2つ生成しています。

using (IObjectResolver objectResolver = containerBuilder.Build())
using (IObjectResolver objectResolver2 = containerBuilder.Build())
{
    // hogeとhoge2は違うインスタンスになる
    HogeClass hoge = objectResolver.Resolve<HogeClass>();
    HogeClass hoge2 = objectResolver2.Resolve<HogeClass>();
}

Lifetime.Transient

Lifetime.TransientではResolveするたびに別のインスタンスが生成されます。

private void Start()
{
    IContainerBuilder containerBuilder = new ContainerBuilder();

    containerBuilder.Register<Logger>(Lifetime.Singleton);
    containerBuilder.Register<Calculator>(Lifetime.Singleton);
    containerBuilder.Register<HogeClass>(Lifetime.Transient); // Lifetime.Transientを使ってみる

    using (IObjectResolver objectResolver = containerBuilder.Build())
    {
        // hogeとhoge2は違うインスタンスになる
        HogeClass hoge = objectResolver.Resolve<HogeClass>();
        HogeClass hoge2 = objectResolver.Resolve<HogeClass>();
    }
}

HogeClassのRegisterでLifetime.Transientを渡すように変えたのでhogeとhoge2は別のインスタンスになります。

LoggerとCalculatorはLifetime.Singletonのままなので、hogeとhoge2のコンストラクタに渡されるLoggerとCalculatorはそれぞれ同じインスタンスになります。これもLifetime.Transientにすれば別のインスタンスが渡されることになります。

Lifetime.Scoped

Lifetime.ScopedLifetime.Singletonに似ていますがIObjectResolverが親子関係を持ったときの動作が異なります。親子関係と一緒に説明するのでここでは省略します。

IDisposableの自動Dispose

IDisposableを実装したクラスをResolveするとLifetimeによっては自動的にDisposeされます。

  • Lifetime.SingletonLifetime.Scopedの場合、IObjectResolverがDisposeされるとResolveで生成されたインスタンスも一緒にDisposeされます。
  • Lifetime.Transientの場合はDisposeされません。IObjectResolverは作りっぱなしなのでDisposeする責任はインスタンスを渡された側になります。

まとめ

Register -> Build -> Resolveの順に使うイメージを持っておいてください。

次回はVContainerの本来の使い方であるLifetimeScopeを通した方法を説明します。


  1. コンストラクタが複数あるときにVContainerがどのコンストラクタを使うか識別するのに使われます。IL2CPPでストリッピングされないようにする効果もあります。省略しても動きますが必ず付ける方が安全です。 

  2. VContainerのコード生成が有効になっているとBuildとResolveのリフレクションが省略されて実行速度が上がります。コード生成についてはいずれ説明します。 

  3. 依存関係が循環すると例外が発生します。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ゲームアクセシビリティの取り組みについて

こんにちは。
個人でゲーム開発をしておりますToya Shiwasuと申します。
この記事では、拙作のオンラインアクションRPGでのアクセシビリティの取り組みについて紹介させていただきます。

アクセシビリティとは

アクセシビリティとは、障害の有無に関わらず、誰でも使える、誰でも使いやすいように工夫することです。
例えば、字幕があれば耳の不自由な方でも台詞の内容がわかりますし、フルボイスなら目の不自由な方でもゲームをプレイできるかもしれません。
最近のゲームとしては、PS4の『The Last of Us Part II』というゲームが、ストーリーに賛否両論はあるものの、高いアクセシビリティで評価されています。

アクセシビリティ対応の経緯

拙作のオンラインアクションRPGは、2018年10月17日にVersion 1.0を公開し、当時は特にアクセシビリティに配慮したゲームではありませんでした。
ほそぼそとアップデートを続けて、2019年12月08日のVersion 2.0で最終エリアを実装し、エンディングまで作り終えました。
さて次に何をしようかとなって、ゲームの3DCG化を検討したりしばし迷走した後、オーディオゲームというものを知りました。
オーディオゲームとは主に視覚障害者向けの音声だけでプレイするゲームで、比較的シンプルなミニゲームから重厚長大な長編のRPGまでいろいろあります。
2020年02月頃からゲームの読み上げ及び3Dサウンドの対応を始めました。
当初は1ヶ月位で対応できるだろうと根拠のない見積もりをしていたのですが、実際にやってみるとUIはすべて作り直しになったり、なかなか大変でした。
そして、8ヶ月後の2020年10月09日のVersion 2.1で「アクセシビリティアップデート」として、公開できました。
その後も、主にアクセシビリティ機能を中心にアップデートを続けています。

視覚障害者向けの機能

視覚障害者向けの機能としては、テキストの読み上げ機能、3D効果音機能、全てのUIの再設計、配色が挙げられます。私自身は晴眼者(目の見える人)ですが、実際に画面を見ずにテストプレイを行い、ソロでの討伐を想定していた中ボスを音だけを頼りにソロで撃破しています。

テキストの読み上げ機能

テキストの読み上げ機能とは、ゲーム中の全てのテキストをOSの読み上げ機能を用いて読み上げる機能です。
残念ながら、Unityの標準機能には音声合成がなかったので、OSの読み上げ機能のラッパーライブラリを自作しました。
初期の読み上げ対応の動画です。
https://twitter.com/ToyaShiwasu/status/1224681290508591104
Version 2.1の時点では、WindowsはC++でSAPIを、AndroidはJavaでTextToSpeechをそれぞれ呼び出していました。
ちなみに、私は実装しなかったのですが、MacはObjective CでNSSpeechSynthesizerを、iOSはObjective CでAVSpeechSynthesizerを使うといいらしいです。
UnityはC#ですので、異なるプログラミング言語で機能を呼び出す、マーシャリングという作業がとてもつらかったのを覚えています。
また、Version 2.2では、要望の多かったスクリーンリーダーのNVDAと呼ばれるソフトへの出力に対応しました。
苦労しつつも、開発者の環境では問題なく動作するものが作れましたが、公開後海外のユーザーからプログラムがクラッシュしたり正常に読み上げないといったバグ報告がちらほらと寄せられます。
調査はしたもののよく分からなかったため、2021年02月15日のVersion 2.3でUnityの有料のアセットであるRT-Voice Proに切り替えました。
こちらもいくつかバグはありましたが、自作した経験が生きて修正はできました。
無事起動できたという報告を受けた一方、使えたボイスが使えなくなったという報告もあり、なかなかままならないものです。

3D効果音機能

キャラクターが縦横に動き回るアクションゲームにおいて、キャラクターの位置を音声だけで伝えるのはなかなか難しい課題です。
Cキーで相対的な方向と距離を読み上げる機能も用意しましたが、戦闘ではリアルタイム性にかけます。
3D効果音機能とは、プレイヤーから見て左で発生した効果音はヘッドホンの左から、右で発生した効果音はヘッドホンの右から、上で発生した効果音は高い音で、下で発生した効果音は低い音で、それぞれ再生される機能です。
左右は特に問題ないですが、上下を音の高低で区別するのは少々直感的ではありません。
しかし、オーディオゲームではよく使われる手法であり、私は慣れれば割と区別できるようになりました。
初期の3D効果音対応の動画です。
https://twitter.com/ToyaShiwasu/status/1225416962827665413
UnityにはAudio SourceにPitchとSpatial Blendという項目があったので、それを使いました。
音圧(音の大きさ)は距離によって減衰するのですが、減衰の仕方は試行錯誤で調整しました。
3D効果音の減衰の仕方の調整の開発記録です。
https://twitter.com/ToyaShiwasu/status/1225795880038486016
開発記録はTwitterに残しているのですが、具体的にどう調整したのかよくわからないですね。
もっと詳しく過程を残しておくべきでした。

全てのUIの再設計

テキストの読み上げ機能とともに全てのUIの再設計を行いました。
レイアウト調整中の動画です。
https://twitter.com/ToyaShiwasu/status/1228610119639789568
特に全盲の方はUIのコンポーネントがどのように配置しているか見えないため、フォーカスの移動方法を分かりやすくするのは重要です。
最終的にはほぼすべてのコンポーネントは上下に配置し、キャラ削除のトグルなどだけ例外的に左右に配置しています。
インベントリスロットは、もともと縦横のグリッド表示だったのですが、上下のリスト表示を追加し、そちらをデフォルトにして、オプションで切り替えられるようにしています。
アイテムウィンドウの所持金などのリスト項目以外の付加的な情報は、Cキーを押すと読み上げるようにしています。
取扱説明書にはCキーでいろいろ読み上げられることを明記していたのですが、ユーザーから所持金はどうやって知るのだという質問が何回か寄せられました。分かりづらかったかもしれません。
オプションにUI倍率を変更する項目もありますが、デフォルトで最大倍率です。

配色

色覚多様性(色覚特性、色覚異常、色盲などとも呼ばれる)の方にとって、ゲームの配色も重要となります。
人の色を感じる錐体細胞には3つの種類があり、障害のある錐体細胞によって色の見え方は異なります。
なので、こうすれば大丈夫といった明確な配色はないのですが、次のサイトに無難な配色の例が掲載されています。
https://jfly.uni-koeln.de/colorset/

このサイトを参考にゲーム内の一部アイコンを修正しました。
https://twitter.com/ToyaShiwasu/status/1290416279258456065
回復アイテムのポーションの種類が色でしか判別できないのは問題だと考え、種類によって突起を付けました。
魔法書アイテムの配色は、無難な配色に修正しています。
転移アイテムの種類が色でしか判別できなかったので、数字を入れました。
オーブという種類のアイテムは、形状を全て変更しました。
ただし、実在する物体の色は変更せずそのままにしています。赤いリンゴは赤く描いていいはずです。

オプションでウィンドウやテキストの配色を変更する機能も実装しています。
https://twitter.com/ToyaShiwasu/status/1288883453132382209
ウィンドウの色は以前は半透明だったのですが、完全に不透明に変更しました。
デフォルトの配色は白黒で、私も若干ダサいとは感じているのですが、一番見やすいはずなので変更する予定はありません。

聴覚障害者向けの機能

聴覚障害者向けの機能としては攻撃マークを表示、オーディオスペクトラムを表示が挙げられます。私自身あまり調査していないこともあって、まだまだ対応は不十分と感じています。また、聴覚障害者は視覚障害者と比べて遊べる市販のゲームも多いこともあって、わざわざ個人開発のゲームが遊ばれるかという問題もあります。まあ、後者はゲームのクオリティの問題なのでこれ以上言及はしません。

攻撃マークを表示

キャラクターが攻撃する瞬間頭上に攻撃マークを表示する機能です。
https://twitter.com/ToyaShiwasu/status/1345646571124211713

オーディオスペクトラムを表示

オーディオスペクトラムを折れ線グラフで表示する機能です。
https://twitter.com/ToyaShiwasu/status/1345696416543383552

その他の機能

その他の機能として、ロックオン機能、多言語対応、ゲーム速度変更が挙げられます。

ロックオン機能

このゲームはもともとヒットアンドアウェイが重要なアクション制の高いゲームとして作成しました。
なので、Version 2.1のアクセシビリティアップデートを公開した時点でも、ゲーム性を損なうのではと考え、ロックオン機能は実装していませんでした。
作者によるテストプレイでも、実際にロックオン機能なしの音のみのプレイで中ボスの撃破まで可能なことを確認しています。
アップデートのたびにちょくちょくテストプレイはしているのですが、そのときにマップの移動や武器の射程内への移動がストレスに感じられました。
なので、特にユーザーからの要望は特になかったのですが、Version 2.3でShiftキーによるロックオン機能を実装しました。
https://twitter.com/ToyaShiwasu/status/1347499546042007554
カーソルキーとの同時押しでマップ移動を行うこともできます。
それと同時に能動防御のアクションを実装し、それまでのヒットアンドアウェイの他に、その場で耐えるという別のゲーム性を模索してみました。

多言語対応

多言語対応は、あまりアクセシビリティの文脈で語られることは少ないのですが、オーディオゲームはまだまだ少ないので、多言語対応を行ってより多くの方に遊んでもらえるようにするのは、意味があると思います。
Version 2.4で12の言語に対応しています。日本語と英語は自分で、ポルトガル語とロシア語はゲームファンの有志の方に、それ以外の言語は機械翻訳を使って翻訳しました。
Unityはアラビア語に対応していないので、とりあえず右から左に表示するようスクリプトを作成しました。ただし、アラビア語は本当は双方向テキストで、非アラビア文字は左から右に表示するのが正しいです。今後の課題です。

ゲーム速度を変更する機能

Version 2.4でゲーム速度を変更する機能を実装しました。
https://twitter.com/ToyaShiwasu/status/1367726769361408002
オンラインプレイの場合、クライアントはサーバーのゲーム速度と同じになります。
オフラインプレイの場合、プレイ中にゲーム速度を変更できます。
Unityの場合、Time.timeScaleを変更することで、簡単にゲーム速度を変更できます。

対応できていない障害と今後の展望

現時点で対応できていない障害もあります。
目が見えず耳も聞こえない盲ろう者が遊べる日本語に対応したオンラインアクションRPGを私は見つけることができませんでした。
点字ディスプレイを使えば技術的には可能なように思われますが、非常に高価ですし解像度も高くありません。
精神障害者とゲームについて話題になることはあまりないように思われます。
意識はあるが体が完全に麻痺している閉じ込め症候群の方が遊べるコンピュータゲームは今の日本にはないようです。
対応にはブレイン・マシン・インターフェースの技術が必要でしょうか。

ゲームアクセシビリティについて、ゲーム開発者が個別に対応する以外の方法も模索されています。
スマホのOCRソフトを利用してゲームのテキストを読み上げる方法があるようです。
https://whiteblackspace.hatenablog.com/entry/2019/10/18/200500
また、ゲーム機のエミュレータに読み上げ機能を統合した例もあります。
https://www.gamespark.jp/article/2019/12/28/95758.html

ゲームを巡るアクセシビリティはまだまだ発展途上と言えるでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む