稲枝の押入れ

いなえが適当なことを書いては、しまっておく場所

DXライブラリを使って、VS2015でゲームのアイコンを変える方法

導入

こんにちは。

ふと、そういえば前々からつけるつけると言ってたゲームのアイコンをつけてなかったなあと思い、DXライブラリを暫くぶりに書いていたのですが、こちら公式情報には「VisualC++ 2005 Express Edition ~ Visual Studio Express 2013 for Windows Desktop の場合」しか書いていない。

自分の環境はVS2015だったので、.exeのアイコンを変えるだけならこのやり方でいけたものの、SetWindowIconID関数を使ったタスクバーやウインドウの左上端に表示されるアイコンの変更はうまくいきませんでした(このページにあるようにresource.hが自動生成されなかった)。

調べたらすぐ出てきたものの備忘録として残しておくと、簡単に言うとさっきのSetWindowIconID関数のページにある「Borland C++ の場合」と同じ方法でいけました。

全体の流れ

つまり全体としては以下の手順で進めると.exeのアイコンとウィンドウタスクバーやウインドウの左上端に表示されるアイコンの書き換え出来ることとなります。

アイコンの準備

1. 縦横32ドット、256色(8ビット)のアイコンファイル(拡張子.ico)を作成する。(『アイコン用ユーティリティ』等でWEB検索するとアイコンを作成するためのソフトが色々見つかるらしいです。GIMPでも書き出せたので僕はGIMPを使いました) 

2. そのアイコンファイルをVisualStudioのプロジェクトファイル(拡張子.sln)が あるフォルダに保存する。

テキストファイルの準備

3. 同様にプロジェクトファイルがあるフォルダに任意の名前をつけたテキストファイルを作成する。

4. そのファイルをメモ帳等のテキストエディタで開き、中身に次のように入力して保存して下さい。

ここに任意の自然数(アイコンID) ICON "ここにアイコンファイルのファイル名"

例えば

333 ICON "icon.ico"

といった感じ。*1

5. 保存したファイルの拡張子がおそらく.txtになっていると思うが、それを.rcに変更する。

VSに追加

6. プロジェクトを VisualStudioで開き、プロジェクト』→『既存項目の追加』で、追加ファイル選択ダイアログを表示して、作成した拡張子が.rcのファイルをプロジェクトに追加します。

7. 次にビルドした時には作成される実行可能ファイルのアイコンがオリジナルのアイコンになるのでそれを確認する。

SetWindowIconID関数を使う

8. SetWindowIconID()関数をゲームの初期化部分に書き、引数に.rcファイルで指定したアイコンIDを渡してやる。さっきの例なら、

SetWindowIconID(333);

と書く。

*1:勿論自然数ならなんでもいけるってわけではない(ハード資源にも限界がある)のでそのあたりを考えた数にしないといけないけどその辺はつっこまないで

__PRETTY_FUNCTION__マクロが使えない

__FUNCTION__

ちょっと気になることがあって小さい規模のコードを書いていたのだが、デバッグの時に今いる関数が表示できたら楽だなと思い、そんなマクロがあった気がするとか思いながら適当な文字列打ってたら__FUNCTION__マクロをサジェストに発見。

しかしこいつは関数の名前しか返してくれない。オーバーロードされた関数が複数あって、その中で__FUNCTION__マクロを使うような事があったら、実際にはどの関数が走っているのかがわからない。

__PRETTY_FUNCTION__

そんなわけで、型や引数の型なんかも返してくれるものないかなと思ったら__PRETTY_FUNCTION__マクロというものを発見。こいつは型や引数の型も返してくれるらしい。

よしこれだと書いてみると

C2065 '__PRETTY_FUNCTION__': 定義されていない識別子です。

とのこと。

こういった所謂「事前定義済みマクロ」はどうもコンパイラの独自実装のことが多いらしいので、そのへんが関係しているのかも?と思ったら、VSのサジェストでもちゃっかり出てきている。でもコンパイラさんはこんな識別子は知らないらしい。

ちょっと調べてみたらg++の独自実装だとか書いてあったけど、ますます何故サジェストが効いたのかわからなくなってきた。

原因が知りたいところではあるけれど、その前にまずは何とか動くようにしたい。

__FUNCSIG__

他にも型や引数の型を返してくれる事前定義済みマクロがないかなあと思って探していたら__FUNCSIG__マクロなるものを発見。これを使ったらうまく行った。

試しにmain関数の中で使ってみたら

int __cdecl main(void)

こんな感じで実行中関数の情報をくれた。

マイクロソフトの公式情報によると、__FUNCSIG__マクロは

__FUNCSIG__ Defined as a string literal that contains the signature of the enclosing function. The macro is defined only within a function. The __FUNCSIG__ macro is not expanded if you use the /EP or /P compiler option. When compiled for a 64-bit target, the calling convention is __cdecl by default. For an example of usage, see the __FUNCDNAME__ macro.

引用元
日本語がいい人はこっち

てな感じらしい。相も変わらず翻訳が意味わからなかったので原文で。まだ日本語訳してくれているだけありがたいので文句ばかりはいってられないけれど、マイクロソフトのあのわかりにくい翻訳は何とかならないのだろうか…

__func__

余談だけれど、「事前定義済みマクロ」はどうもコンパイラーの独自実装のことが多い、とさっき書いたけれど、その中でもこの関数名を表示する系のもののうち一部は、今では(という書き方をすると怒られそうなので補足しておくと一定の規格からは)規格として定義されているらしい。

例えば、C99では__FUNCTION__と同等の機能のものが__func__として追加されたそうな。ただし、

C++では現時点では定義が無いが、g++やclang/LLVMではC++コンパイル時にも利用できる。 g++の場合、C++で使用した場合は __FUNCTION__ とは違い関数名や引数はクラス名付きで得られる。

引用元

ということで、C++だと同等とは言えないみたいだけど。

まとめ

簡単にまとめると

  • __FUNCTION__
    • 関数の名前のみを得られる
  • __func__
    • C99で定義
    • __FUNCTION__と基本的に同じ
    • C++環境では違うことも?
  • __FUNCSIG__
    • 関数の型と名前と引数の型を得られる
  • __PRETTY_FUNCTION__
    • 関数の型と名前と引数の型を得られる(らしい)
    • VS環境ではコンパイル時にエラーが出て使えない(サジェストには表示される)
  • __FUNCDNAME__
    • 関数の名前を装飾名付で得られる

という感じ。一番下のものはこの記事では触れてませんが気になったら調べてみてください。さっきのMSのページにも書いてます。

あと、ここではわかりやすいように「得られる」と書いたが、厳密には「定義されている」だと思う。関数ではなくマクロなので。

もし__PRETTY_FUNCTION__が僕のVS環境下でコンパイルするとエラーが出るのか、思い当たる人は是非教えていただきたいです…

VisualStudioでコンソールプログラムを実行後、コンソール画面を消さない方法

調べた感じVS2010からどうも「空のプロジェクト」を作るとデフォの状態だとプログラムの実行が終わると自動でウィンドウが消えてしまう様になっているらしい。

最近はGUIの開発が多かったので知らなかったのだけれど、ちょっと試したいことがあって久々にコンソールプログラムを作って実行してみたら一瞬でウィンドウが消えて結果が全く見えなかった。

というわけで、それをどう直したらいいかを調べるのがかれこれ3回目くらいなので、大した分量もないのだが大まかに3つの方法を備忘録的に書いておく。

system(“pause”);

まず、そもそも調べるのが面倒くさい時はsystem関数を使ってしまう。これは簡単に言うと文字列を受け取ってその文字列をコマンドプロンプトに打ち込んだのと同じような動きをしてくれる関数だ。

つまり、system("pause");と書くと、任意のキーを押すまで処理を中断するコマンドであるpauseコマンドが実行されるのだが、これをプログラムの最後においておけば、処理が中断されてプログラムが終わらないのでウィンドウが消えなくなる(勿論キーを押すと終わってしまう)。

また、この関数を使うにはC言語なら<stdlib.h>C++なら<cstdlib>をインクルードする必要がある。ただ、<iostream><random>等を辿っていくとその中でそれらがインクルードされていたりするので、ユーザーが自らインクルードする必要まではない場合もある。

ブレークポイント

2つめにもっと面倒くさい時、と言うか特にコードを汚したくない時はVSの機能であるブレークポイントを使ってしまう。これをプログラムの終わりのあたりに設定しておき、デバッグありで実行すれば、おそらくブレークポイントのところにプログラムが達した段階でVSの方に戻ってきてプログラムが一度止まる。するとプログラムは「デバッグの停止」をするまで止まるので、それで出力が確認できる。

設定を変える

もしくはそもそも設定を変えることでこれを回避できる。少し長い間使うプロジェクトならこっちのほうが楽だ。これだとデバッグなし実行(Ctrl+F5)をした時に勝手にプログラムが終了するのを防げるようになる。

やり方は、 1. ソリューションエクスプーラーで設定したいプロジェクトを右クリックしてプロパティを開く 2. 左のボックスの中から、構成プロパティ/リンカ/システムを選ぶ 3. 右のボックスから一番上のサブシステムコンソール(/SUBSYSTEM:CONSOLE)を選択

以上と言った感じなんですが、何でデフォの設定変わったんですかね。

distributionで乱数の幅を決める時、負の値が指定できない

追記: 自分のアホでミスっていただけでした。結論から言うとresultをunsigned intにしているのに気付いていないままコードを書いていただけでした…お騒がせしました…
よって、この記事は情報が0に近い記事になってしまったわけですが、同じようなうっかりさんが居るかもしれないんのでこのまま残しておきます。

色々あって精度のそれなりな乱数を使いたいなって事でC++11から入ったメルセンヌ・ツイスタを使って乱数生成クラスを作ってたんですが、テストコードを走らせてる段階で何か少し変な挙動を発見。

コードの規模を小さくして検証してみると、どうもdistributionの動きが僕が思ってるのと違うっぽい?

以下0~100の範囲で乱数を100回生成させるようなコードなのですが、これはまあ思った通りに動く。 <コード>

#include<iostream>
#include<iomanip>
#include<random>


int main() {

    std::random_device rand_seed;//非決定論的乱数生成器
    std::mt19937 mt_obj(rand_seed());//メルセンヌ・ツイスタで乱数を生成する
    std::uniform_int_distribution<int>::param_type uniform_parameter(0, 100);//整形する幅を決める
    std::uniform_int_distribution<int> uniform(uniform_parameter);//生成された乱数を指定された幅の一様分布に整形する

    static int calc_num = 0;
    unsigned int result = uniform(mt_obj);


    for (int i = 0; i < 100; i++) {

        if (i != 0) {//初回は飛ばす
            result = uniform(mt_obj);
        }

        calc_num++;

        //表示の幅やレイアウトについて指定して出力している
        std::cout
            << std::setw(5) << std::left << calc_num
            << ":result = "
            << std::setw(5) << std::right << result
            << std::endl;
    }
    return 0;
}



<実行結果>

1    :result =    77
2    :result =    64
3    :result =    63
4    :result =    87
5    :result =    22
6    :result =     3
7    :result =    48
8    :result =    62
9    :result =     0
10   :result =    42
11   :result =    78
12   :result =    52
13   :result =    64
14   :result =    37
15   :result =    51
16   :result =    94
17   :result =    14
18   :result =    24
19   :result =    86
20   :result =    16
21   :result =    25
22   :result =    53
23   :result =    30
24   :result =    45
25   :result =    48
26   :result =    47
27   :result =    17
28   :result =    54
29   :result =    85
30   :result =    99
31   :result =    76
32   :result =    18
33   :result =   100
34   :result =    15
35   :result =    18
36   :result =    17
37   :result =    37
38   :result =    58
39   :result =    77
40   :result =    59
41   :result =    50
42   :result =     4
43   :result =    54
44   :result =    27
45   :result =    66
46   :result =    41
47   :result =    57
48   :result =    63
49   :result =    43
50   :result =    75
51   :result =    10
52   :result =    58
53   :result =    24
54   :result =     3
55   :result =     6
56   :result =    97
57   :result =    37
58   :result =     8
59   :result =    74
60   :result =    35
61   :result =    46
62   :result =    88
63   :result =    66
64   :result =    30
65   :result =    93
66   :result =    33
67   :result =    50
68   :result =    43
69   :result =    19
70   :result =    88
71   :result =    93
72   :result =    63
73   :result =    57
74   :result =    42
75   :result =    87
76   :result =    51
77   :result =    44
78   :result =    62
79   :result =    19
80   :result =    88
81   :result =    10
82   :result =    11
83   :result =    40
84   :result =    12
85   :result =    38
86   :result =    90
87   :result =    11
88   :result =    18
89   :result =    42
90   :result =    78
91   :result =    38
92   :result =     2
93   :result =    35
94   :result =    44
95   :result =    92
96   :result =    92
97   :result =    25
98   :result =     0
99   :result =    18
100  :result =    74

ところが、distributionの範囲にマイナスを含むようにすると動きが少しおかしくなる。

以下は-100~100の範囲で乱数を100回生成するコードだと… <コード>

#include<iostream>
#include<iomanip>
#include<random>


int main() {

    std::random_device rand_seed;//非決定論的乱数生成器
    std::mt19937 mt_obj(rand_seed());//メルセンヌ・ツイスタで乱数を生成する
    std::uniform_int_distribution<int>::param_type uniform_parameter(-100, 100);//整形する幅を決める
    std::uniform_int_distribution<int> uniform(uniform_parameter);//生成された乱数を指定された幅の一様分布に整形する

    static int calc_num = 0;
    unsigned int result = uniform(mt_obj);


    for (int i = 0; i < 100; i++) {

        if (i != 0) {//初回は飛ばす
            result = uniform(mt_obj);
        }

        calc_num++;

        //表示の幅やレイアウトについて指定して出力している
        std::cout
            << std::setw(5) << std::left << calc_num
            << ":result = "
            << std::setw(5) << std::right << result
            << std::endl;
    }
    return 0;
}



<実行結果>

1    :result =   100
2    :result =    72
3    :result =    13
4    :result =    12
5    :result = 4294967282
6    :result =     9
7    :result =    67
8    :result =    91
9    :result =    25
10   :result =    39
11   :result = 4294967232
12   :result =     5
13   :result =    67
14   :result =    45
15   :result = 4294967221
16   :result = 4294967211
17   :result = 4294967266
18   :result =    35
19   :result = 4294967293
20   :result = 4294967256
21   :result = 4294967258
22   :result = 4294967197
23   :result =    68
24   :result =    46
25   :result =    83
26   :result =    71
27   :result = 4294967256
28   :result =     1
29   :result =    63
30   :result = 4294967235
31   :result =    97
32   :result = 4294967291
33   :result = 4294967202
34   :result =    45
35   :result = 4294967272
36   :result =    17
37   :result = 4294967287
38   :result =    33
39   :result = 4294967288
40   :result =     3
41   :result = 4294967271
42   :result =    52
43   :result =    31
44   :result =    16
45   :result = 4294967278
46   :result = 4294967295
47   :result = 4294967204
48   :result =     9
49   :result = 4294967270
50   :result =    44
51   :result = 4294967273
52   :result =    88
53   :result = 4294967221
54   :result =    36
55   :result = 4294967250
56   :result =    29
57   :result =    98
58   :result =    58
59   :result = 4294967277
60   :result = 4294967246
61   :result =    86
62   :result =    66
63   :result =    82
64   :result = 4294967212
65   :result =    60
66   :result = 4294967268
67   :result =     3
68   :result =    83
69   :result =     5
70   :result = 4294967291
71   :result = 4294967263
72   :result = 4294967288
73   :result =    73
74   :result =    88
75   :result = 4294967236
76   :result =     6
77   :result =    39
78   :result =     3
79   :result =    62
80   :result = 4294967212
81   :result = 4294967200
82   :result =    17
83   :result = 4294967224
84   :result =    75
85   :result =     5
86   :result = 4294967289
87   :result =    29
88   :result = 4294967213
89   :result =    59
90   :result = 4294967253
91   :result =     0
92   :result =    10
93   :result = 4294967289
94   :result =    49
95   :result =    37
96   :result =    94
97   :result =    45
98   :result = 4294967293
99   :result = 4294967259
100  :result = 4294967221

と、実行結果がdistributionで定めた範囲の一様分布から外れている。これは負の値はdistributionの範囲に指定できないということなのだろうか?

ただ、std::uniform_int_distribution<int>::param_typeのコンストラクタでは幅の最小最大についてunsigned intではなくintを引数に取っているし、param_typeを使わずにstd::uniform_int_distribution<int>のコンストラクタもunsigned intではなくintを引数に取っている。

仮にこのdistributionが負の値を範囲指定出来ないなら、この辺がunsigned intで引数に渡せと言われそうなものだけど、これは本当に負の値を範囲指定できないという解釈で良いのだろうか。

もちろん、欲しい乱数の幅をズラすのは、得られた範囲にズラしたい分の幅を足したり引いたりすればいい(例えばdistributionの幅を0~200にしておいて、result = uniform(mt_obj);result = uniform(mt_obj) - 100;というようにして、得られた値を-100すれば、-100~100の範囲で乱数は得られる)

でもこんなの標準で出来るようにしておいて欲しい、という願望もあるので、何かできる方法は無いのかなあと思うものの、やはりよくわからない。 `

誰か知っていたら教えて下さい。

VisualStudioでプロジェクトのファイル名等を変える



!!!!注意!!!!

この記事は僕が自己の備忘録も兼ねて書いたものです。自分はプロでもなんでもないのでおそらく間違いもあると思います。 そのため、この記事に基づいてリネームをしようという場合、必ずソリューションフォルダをバックアップしておいてください。 また、ここに書いてあることを元に何かをして何かが起きても僕は一切の責任を取りません。誤りは無いように努力してはいますが、過信せず自分でも調べたり考えたりしながらお読みください。 また、Gitなどのバージョン管理を使っている方はリネームでフォルダの名前を変えたりもするのでで少し面倒な事になるかもしれませんが、そこも自己責任でお願いします。まあ手動で直せる範囲だとは思いますが。 また、間違った点については指摘をくださると、僕も勉強になるので助かります。



前提

出展

この記事はいなえがQiitaに上げた記事があまりにもわかりにくかった為、加筆修正を加えて再編成したものです。

例を持ち出す際に想定されるファイル構成

※拡張子の書いていないものはフォルダ

  • Project
    • RPG(ソリューションフォルダ)
      • RPG.sln
      • RPG.sdf
      • RPG.suo
      • RPG(プロジェクトフォルダ)
        • RPG.opensdf
        • RPG.sdf
        • RPG.vcxproj
        • RPG.vcxproj.filters
        • RPG.vcxproj.user
        • RPG.suo
        • (その他略)
      • (その他略)
    • (その他略)

そもそも何をしたいのか

目的

あるソリューションにおいて、

  1. ソリューション名とプロジェクト名
  2. フォルダ名とファイル名

のそれぞれを新しいものに変更することを目指します。

理由

現行の名前と以前の名前が混在していると統一感がなく混乱を招き得るので。

やり方

簡潔なまとめ

以下長々と色々書いていくのですが、自分で読んでも読むのが辛い駄文ゆえ、簡潔に手順をまとめておこうと思います。

  1. VisualStudioのソリューションエクスプローラーでソリューション名とプロジェクト名を変更する。
  2. 保存してVisualStudioを落とす
  3. .slnファイル(ソリューションファイル)をテキストエディタで開いて、プロジェクトフォルダと.vcxprojファイルへの関連付けを変更する
  4. ソリューションフォルダやプロジェクトフォルダをはじめ、拡張子が
    • .vcxproj.filters
    • .suo
    • .sdf
    • .opensdf
    • .vcxproj.user などのファイルを含め、すべてのファイル名を変更する。

ここでの「変更する」とは「現行の名前から新しい名前に書き換える」ということです。

ソリューション名とプロジェクト名の変更

VisualStudio(以下VS)ではソリューション名やプロジェクト名を変更する際、ソリュションエクスプローラで名前の変更をすれば簡単にそれらの名前を変更することが出来ます。ソリューション名とプロジェクト名の変更についてはこれで終了です。

ここで注意したいのは、ソリューションエクスプローラソリューションの名前を変更すると、拡張子が".sln"のファイル(ソリューションファイル)の名前が変更されることです。これとは反対に、プロジェクトの名前を変更しても、.vcxprojファイルの中に記されたプロジェクトファイルへの関連付けが変わるのみ*1で、ソリューションファイルの名前以外、どのファイル・フォルダの名前も変わりません。

フォルダ名とファイル名

上記変更はあくまでVS上で扱うソリューション名やプロジェクト名の変更でしかなく、(ソリューションファイルの変更を除き)それらのあるフォルダやファイルの名前は変更されません。 それが何か悪いの?という話になるのですが、次の場合を見てみてください。

例えば仮にRPGゲームを作ろう!!とC:\Users\you\Documents\Visual Studio 2015\Projects\RPGというソリューションを作り、開発を始めるとします。具体的にはその中のRPGプロジェクトを進めていくことになるのでしょうが、開発が進むに連れてタイトルが決まったりすると思います。ここでは「おさげ少女と勇者の大冒険」という名前に決まったとしましょう。そこで、このタイトルに合わせてせっかくだからソリューション名やプロジェクト名も「OsageRPG」に変えてしまおう!となったとします。

VS上でソリューション名やプロジェクト名を変えても、ファイル名の変更は"RPG.sln"が"OsageRPG.sln"に変わるだけで(隠しファイルの中などどうも実は一部変わっているっぽいが)その他には変化はありません。

そうすると、VS上はプロジェクト名やソリューション名は"OsageRPG"になっているのに、ディレクトリ上は殆どがRPGの名前のままで(しかもソリューションファイル名だけ"OsageRPG.sln"!!)統一性がなく、不格好なだけでなく混乱の元になりかねません。

※此処から先ではとりあえずVS上でソリューション名やプロジェクト名をVSから変更していない状態(=ソリューションファイル名が"RPG.sln"のままの状態)であるとして話を進めます

じゃあ直接エクスプローラーやらでフォルダ名やファイル名を変えてやればいいじゃないか

と思うでしょうがそう上手くは行きません。Project直下のRPGディレクトリに関しては問題無いでしょうが、"Project\RPG\RPG"(つまりはソリューションフォルダ直下のプロジェクトフォルダ)の名称をエクスプローラーで直接"Project\RPG\OsageRPG"変えてやったとしましょう。すると、フォルダ名を覚えている"RPG.sln"が「"Project\RPG\RPG"を探してるんだけど無いな。このソリューションおかしいぞ?」と"Project\RPG\OsageRPG"を見つけ出せずエラーを出します。

これをなんとかして解決して、このソリューションフォルダの中のファイル名の"RPG"となっている部分を"OsageRPG"何とか変更できないか、というのが今回の一番大事なお話です。

早い話の結論

グーグル先生は偉大

こういうことはやはり他の誰かもしたがっているもので、当然情報が転がっていました。

まず"RPG.sln"が探そうとするプロジェクトファイルを"Project\RPG\RPG"ではなく"Project\RPG\OsageRPG"に書き換えてやれば、エクスプローラーでプロジェクトフォルダの名前を変えてやっても問題なさそうです。そこで、RPG.slnテキストエディタで開きます。

すると、

Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RPG", "RPG\RPG.vcxproj", "{BE0A076E-539C-40BE-977E-7125AEEECEE7}" EndProject

と書いてある部分があると思います。

これを

Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OsageRPG", "OsageRPG\RPG.vcxproj", "{BE0A076E-539C-40BE-977E-7125AEEECEE7}" EndProject

と書き換えてやればプロジェクトフォルダの名前をエクスプローラーで"OsageRPG"に変更しても"RPG.sln"がちゃんと見つけてくれるようになります。(実は一つ目の" "内に書かれている名前はどうもVSが内部的に使っている名前のようなので、変えなくても見つけてはくれるみたいです。気になるので変更しましたが。)

でもまだ前の名前のままのところもあるよね?

気になりません?

フォルダ名のみを変えたいという人は上の部分までで終わりです。でもきっと、「まだ"OsageRPG"になってないところがあるじゃないか!!」と気になってしまう人もいると思います。例えば"Project\RPG\OsageRPG"直下にある"RPG.vcxproj"や"RPG.vcxproj.filters"など…

ではこいつらもおさげにしてやりましょう!!("OsageRPG"にリネームしよう、の意)

おさげだ!

VS上で既にソリューション名を"OsageRPG"にしている場合、.slnファイルは既におさげになっているでしょう。よってその他の、さっき例に上がった、"Project\RPG\OsageRPG"直下にある"RPG.vcxproj"等をリネームしていきましょう。

どうせこうするんでしょ?

お察しの方も多いかと思いますが、こいつも単に"OsageRPG.vcxproj"などとエクスプローラーでリネームしてしまうと、エラーが出ます。ではどうすればいいのか?これも気付いている方もいらっしゃるかと思いますが、もう一度"OsageRPG.sln"をテキストエディタで書き換えます。

ここまでで

Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OsageRPG", "OsageRPG\RPG.vcxproj", "{BE0A076E-539C-40BE-977E-7125AEEECEE7}" EndProject

上記の様になっているかと思いますが、これを

Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OsageRPG", "OsageRPG\OsageRPG.vcxproj", "{BE0A076E-539C-40BE-977E-7125AEEECEE7}" EndProject

のように書き換えます。その後、"Project\RPG\OsageRPG"直下にある"RPG.vcxproj"を"OsageRPG.vcxproj"にエクスプローラーでリネームすればOKです。

やったぜ!!

と言いたいところですが、これでVSを起動すると、VS上でソースコードをフォルダ分け等をして整理して管理していた情報等は失われソースコードが整理もされずにプロジェクト内に入った状態になります。これをもう一度フォルダ分けしていったりするのは、正直面倒ですよね…

しかし大丈夫

"Project\RPG\OsageRPG"直下にある"RPG.vcxproj.filters"がこのフォルダ分けなどの情報を持っているのでこいつを"OsageRPG.vcxproj.filters"とリネームしてやればVS上では今までどおりの状態に戻ります。

仕上げ

残党狩り

さて、大体のリネームは終わりましたが、一部まだおさげになっていないファイルたちがいると思います。そいつらもおさげにしてやりましょう。

ソリューションフォルダ

Projectフォルダ直下のソリューションフォルダは普通にリネームしても構いません。ここまで"RPG"のままにしていたのは説明で混乱を避けるためでした。

ソリューションフォルダ直下

おそらく.suo.sdfという拡張子のファイルが未だにリネームがされていないと思います。

.suoは「ソリューションユーザーオプション」の略で、VSで行ったブレークポイントなどのユーザーレベルのカスタマイズ情報を持っています

.sdfインテリセンス用のキャッシュDBファイルです。おそらく「ソリューションデータベースファイル」とかその辺の略じゃないでしょうか。
.sdfは放っておいて開発していても自動で何処かのタイミングでリネーム後の名前のものが生成されると思いますが、何にしてもどちらもエクスプローラーでリネームして構いません。

プロジェクトフォルダ直下

.opensdfがリネームされていないかと思いますがこれもリネームして構いません。
.vcxproj.userもリネームされていないかと思いますが、これはデバッグと配置の設定などのユーザー固有のプロパティが格納されたもので、特定のユーザーのすべてのプロジェクトに適用されます。これもリネームして構いません。

おしまい!

おそらくこれですべてのリネームが完了したでしょう。お疲れ様でした。.exe等は残ってるかもしれませんがその辺りは当然わかると思いますので自分でお願いします。

余談

リネームソフト

ここまで手動でのリネームの仕方を書いておいてなんですが、名前を指定したら自動でリネームしてくれるVisual Studio Project Renamerというソフトが既にあるそうです。 僕は使ったことがないのでよくわかりませんが、おそらくこのソフトを使ったほうが楽でしょう。使ってみて良かったら教えて下さい。

namespaceは変更されていない?

.vcxprojファイルをテキストエディタで開いてみると<RootNamespace>タグで囲まれた部分が旧名称のままになっています。ここを手動で書き換えて良いのかは調べてもよくわからなかったのですが、気になる人は試してみても良いかもしれません。名前からするにrootnamespaceの設定だろうので問題は無いとは思うのですが…

参考文献

http://d.hatena.ne.jp/oira3ryu/20110828/1314518629

https://blogs.msdn.microsoft.com/vcblog/2010/03/09/intellisensebrowsing-options-in-vc-2010/

https://msdn.microsoft.com/ja-jp/library/xhkhh4zs.aspx

https://msdn.microsoft.com/ja-jp/library/ee862524.aspx

http://stackoverflow.com/questions/12188975/how-to-rename-the-name-of-project-in-vs2010

*1:.vcxprojファイル内のタグで囲まれた部分が変わる

VisualStudio2015に後からC++を追加する

新年

あけましておめでとうございます、いなえです。

全く新年感はないタイトルではありますが、今年もよろしくお願いします。

さて、さっそく本題に入りましょう。

経緯(飛ばしていいです)

僕が主にプログラミングをするPCとして、我が家にはデスクトップPCとノートPCが各1台ずつあります。そのそれぞれにはVisualStudio(以下文中ではVS)が入っているのですが、

  • デスクトップPC
    • VisualStudio2015
      - C++
      - C#
      
  • ノートPC
    • VisualStudio2013
      - C++
      
    • VisualStudio2015
      - C#
      

という感じの環境になっていました。 なんで2013と2015が併存しとんねん!という事なんですが、2013はProfessionalだったので、2015のCommunityとの機能差がわからず、こっちの方が高性能だったりしないか?と残しといたのが1つ、VS2013で作ったプロジェクトがいくつかあり、VSのバージョン間の違いに絡んだ面倒な問題が起きたりした時に最悪ノートPCの方で直せるようにしておきたいと思っていたのがもう1つの理由です。

今回はこの環境のノーパのVisualStudio2015(Community)C++を追加した時の手順について書いています。備忘録です。また、OSはWindows8.1です。

いや、C++ならVS2013使えや、という指摘があるかもですが、今回、VS2015に対応したSiv3Dを使ってみたかったのと、さっきも書いたようにVSのバージョン間の違いに絡んだ面倒な問題があるといやだなという事でVS2015にC++を追加しました。

やり方

まず、コントロールパネルを開き、そこからプログラムプログラムと機能からプログラムのアンインストールまたは変更という画面を開き、VS2015を一覧から探し、右クリックをして変更をクリックします。

f:id:makiofinae:20170104171027p:plain

するとこのような画面が開きます。Modifyをクリックします。 f:id:makiofinae:20170104171535p:plain

そうすると以下のような画面になります。 f:id:makiofinae:20170104171544p:plain

Programming Languagesの左のドロップダウンボタン(▷の形をしたボタン)を押すとおそらく追加されていないリストが表示されるので、

f:id:makiofinae:20170104171550p:plain

目的の言語(ここではVisual C++)のチェックボックスをクリックします。そしてそれが出来たら右下のNextをクリックします。

f:id:makiofinae:20170104171556p:plain

すると確認画面が出るので、自分が選択したものがそこにあるか、逆に選択していないものがそこにないかを確認したのち、下に表示されている容量を使っても問題ないかを確認し、右下のUPDATEを押します。

f:id:makiofinae:20170104171602p:plain

余談

これで終了です。

どういう理由があったにしても、「VSの新バージョンをインストールするのに自分がC++を入れないのは信じられないなあ。なんではいってなかったんだろう。」と思ったらVS2015はデフォルトでC++は入らないようになってるんですね。C#を普及させたいマイクロソフトの思惑でもあるのでしょうか。

この辺の操作自体は簡単なのですが、流石に容量が容量なので、ダウンロードとインストールに結構時間がかかります。急いでいる時はvim等の軽量なエディタを使った方がいいかもしれません。

Unityでゲームパッドから多人数入力を受け取る方法まとめ

目次


初めに

前置き

どうも、いなえのまきです。

僕については

inaenomaki.hatenablog.com

この辺を見てもらえると良いと思いますので、自己紹介はこのくらいにしておきます。

この記事はUnityについてのものですが、僕の環境がUnity 5.4.0f3なので、それを想定して書いています。

また、初心者ゆえに間違いがある部分があるかと思いますが、その点については指摘していただけると助かります。

CCS Advent Calender

この記事はCCS Advent Calendar 2016の21日目として書かれた記事です。

前の記事 by こまつな

次の人→まっそうめん


動機

皆さん、ゲームを作るのは楽しいですよね。でも、それと同じかそれ以上に難しいとも思います。面白いゲームを作ることの難しさというのを僕はこの何年かでとても感じました。面白いゲームを作るには様々な要素について様々な視点から考え、その上で作りたいゲームのコンセプトに合致した決定をしていかないといけません。正直、大変な作業です。
これを楽にする方法の1つに、多人数プレイにしてしまうという方法があります*1。シンプルなゲームルールでも、多人数対戦等にすると良ゲーと化したりします。みんなで盛り上がりながらゲームをする楽しさというのは、単純なそのゲームの楽しさ以上のものがあると思います。
そして多人数ゲームにしてしまうと、AIの実装など、一部の実装について必要がなくなる可能性があります。開発が楽に越した事はないですよね*2
しかし、多人数プレイのゲームを作るとなると、2人ならいいですが、人数が増えるにつれてキーボードやマウスのみの操作では辛くなってきます。例えば4人で一つのキーボードを押してプレイするなんて狭くてたまったものじゃありません(遠い目)

複数人対戦型おでんゲー「ODEN FIGHT!!(仮)」*3。誰か良いタイトルください。 f:id:makiofinae:20161216205310p:plain f:id:makiofinae:20161216205306p:plain

そうなるとゲームパッド(よくあるゲームのコントローラー)で操作できるようにするのがいいと思うのですが、その多人数対戦ゲームをUnityで作ろうとすると色々と調べることになると思うのですが、少し情報が分散して手間がかかります。もしかすると人によっては諦めてしまうかもしれません。この記事は、そういった人に多人数ゲームを作ってもらえたらなという思いから、入力関係についてまとめた記事です。

その為大して新規性はありません。ただ、この記事を見れば多人数対戦ゲームが作れるように纏めたつもりです。

InputManager

InputManager

Unityでは入力関係を扱うクラスとして、Inputクラスというものが用意されています。Unityを使ったことのある方なら一度は使うであろうこのInputクラスの管理をする為に、UnityではInputManagerというものが用意されています。このInputManagerの設定等を受け、Inputクラスは動きます。ではこのInputManagerでは何が出来るのでしょうか?

仮想入力

InputManagerでは、仮想の入力を扱います(仮想の入力について、以降『仮想入力』と書きますが、これは説明がしやすいように僕が便宜的に読んでいるだけで日本語では正式にどう呼ばれているのかは僕は知りません)。急に言われてもよくわからないですね。仮想入力とは、実際の入力とゲームの入力受け付けとを結びつける概念のようなものです。どういうことかというと、例えば2DアクションゲームでZキーを押したらジャンプする様な処理をする場合、

  • Zキーを押したか→押していたらジャンプ

という風にコード上で書くのが通常かと思うのですが、これをUnityでする場合、

  • "Jump"の仮想入力があったか→あったらジャンプ

という風に書いた上で、"Jump"の仮想入力とZキーの入力とを紐付けることによって実現します。

f:id:makiofinae:20161220214729p:plain

仮想入力を使うメリット

変更が楽

これによるメリットは何か、というと、まず操作方法が変わった場合に楽に変更できる、という所にあります。 上の例だと分かりにくいので設定を増やしましょう。タイトル画面、ゲーム中のポーズ画面、ゲームオーバー時のリトライ画面でも、Zキーを押した時に決定をしたことにする、とします。 操作に使うキーやボタンというのはそのゲームに合わせて必要最小限にとどめるべきであり、また統一したゲーム操作感、プレイヤーが期待するであろうボタンを押した時の動作等を保証するためにもこの様に1つのボタンで様々なところでの操作を行う場面はあるかと思います。

さて、この様な場合にZキーでしていた動作をやっぱりXキーでするようにしよう、となったとします。もし「Zキーを押したか→押していたら処理」という方式を取っていた場合、ジャンプの処理や上記決定の処理の部分の「Zキーを押したか」の部分を「Xキーを押したか」に書き換える必要があります。こうすると書き換え漏れが起きたりといった問題が起きるかもしれません。

『"Jump"の仮想入力があったか→あったら処理』という風に書いた上で、"Jump"の仮想入力と実際のキーの入力とを紐付ける」という方式をとっていた場合、"Jump"の仮想入力に紐つけるキーをZからXに紐つけるだけで済みます。 また、仮想入力には例えばここでは"Jump"等の名前がつけられるので、その仮想入力がどういった場面で使われるものなのかがわかりやすくなります。(この点から考えると上記決定の処理については"Jump"では無い新しい仮想入力を作ってそれを割り当てるべきかもしれない。Mainとか?)

f:id:makiofinae:20161220214801p:plain

キーコンフィグが使える

そしてもう一つ、大きなメリットとして、仮想入力に紐つける実際の入力については、プログラムの起動時に出るウィンドウで設定することが出来るということです。これによって、特に用意しなくてもキーコンフィグが実現されます。実際の入力を好きな仮想入力に紐付けられるので、本来はキーボード操作を想定しているゲームであっても、ゲームパッドの入力を仮想入力に紐つけることでゲームパッドでプレイすることも可能になります。これは個人的には非常に大きなメリットかなと思います。

f:id:makiofinae:20161216175323p:plain

こんな感じのウィンドウが起動時に出て、ここで設定が可能です

実際の設定の方法

InputManager概観

仮想入力についてそれとなく話したところで、InputManagerを実際に開いてみましょう。上のバーからEdit->Project Settings->Inputをクリックします。 f:id:makiofinae:20161216174755p:plain

そうするとInspecterにInputManagerの中身が表示されると思いますAxesを開くと、中にはSizeという項目と、そこから下にHorizontal等の項目があると思います。このSizeという項目の値が、合計で何個仮想入力を作成してあるかの値です。そしてそれ以外の、Horizontal等の項目の一つ一つが、仮想入力の設定になっています。 f:id:makiofinae:20161216174757p:plain f:id:makiofinae:20161216205714p:plain

仮想入力の作り方

Sizeの値が、デフォルトで18とかになってると思うので、仮想入力を新しく1つ作りたい時は19と書き換えれば仮想入力が1つ増えます。  f:id:makiofinae:20161216181427p:plain f:id:makiofinae:20161216181431p:plain

下に一つ増えた!!

また、既存の仮想入力を右クリックする事で複製して増やしたり、逆に削除して減らすことも出来ます。右クリック->Duplicate(複製)もしくは右クリック->Delete(削除)と操作すると可能です。
f:id:makiofinae:20161216174745p:plain

ソースコード側での話

Inputクラス

さて、仮想入力の作り方が分かったところで、それをソースコードからどう使うのかについて説明したいと思います。ソースコードからはInputクラスを通じて仮想入力を取得します。Inputクラスはその名の通り入力一般を扱うクラスです。そのメンバ関数として仮想入力を取得する関数を持っているので、それを使います。入力を取得する関数には、大別して主なもののみあげると、仮想軸入力を扱う関数、仮想ボタン入力を扱う関数、キー入力を扱う関数、マウスボタン入力を扱う関数の4つがあります。正確にはまだあるのですが、本題からはそれるので気になる人は調べてみてください。さて、この中で今回は仮想軸入力仮想ボタン入力、つまり仮想入力を扱う関数について見ていきたいと思います。

仮想入力を扱う関数

仮想ボタン入力

それでは実際に見てみましょう。まず、仮想ボタン入力について考えてみましょう。そもそもボタン入力とは何か、というと押している/押していないの二つの情報をもつ入力です。つまり仮想ボタン入力とは仮想ボタンが押されているかどうかの情報をもつ入力なのですが、Unityではその仮想ボタン入力について、今押されたところであるか/押されている最中であるか/今離されたところであるか、の3つを取得することができます。それぞれ対応する関数はGetButtonDown/GetButton/GetButtonUpという名前になっています。引数にInputManagerで作った仮想ボタン入力の名前の文字列を渡してやることで、その入力がそれぞれの関数で判定される入力の状態を真理値で得ることができます。つまり

        if (Input.GetButtonDown("GamePad1_Jump") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押し下げられた時にする処理
        }

        if (Input.GetButton("GamePad1_Jump") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押されている間ずっとする処理
        }

        if (Input.GetButtonUp("GamePad1_Jump") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが離された時にする処理
        }

と書いて使います(GamePad1_Jumpは仮想ボタン入力名)*4

以下にそれぞれの関数がどういう動きをするかを表にしておいたので参考にしてください。

f:id:makiofinae:20161220220422p:plain

仮想軸入力

では次は仮想軸入力について見てみましょう。そもそも軸入力とは何か、というと正負どちらの方向にどの程度の入力があるかの情報を持つ入力です。単に入力があるかないかについて見るのみでなく、入力がある際にその入力の大きさ等についても見るところがボタン入力とは違います。入力軸一つごとに入力に対する数直線があると考えるとわかりやすいかもしれません。この入力軸の情報を取得するためにはGetAxis関数もしくはGetAxisRaw関数を使います。

GetAxisRaw関数は引数にInputManagerで作った仮想軸入力の名前の文字列を渡してやることで、その仮想軸入力の値を符号付き少数型で得ることができます。返ってきた値が正の値なら軸の正の方向に、負の値なら負の方向に入力があったことを意味します(一部例外あり)。これは例えばゲームパッドのアナログスティックの入力などを取得する時に使います。つまり

        if (Input.GetAxisRaw("GamePad1_X") > 0)
        {
            //ゲームパッドの移動入力のx軸の正の方向(右方向)の入力がある時にする処理
        }
        if (Input.GetAxisRaw("GamePad1_X") < 0)
        {
            //ゲームパッドの移動入力のx軸の負の方向(左方向)の入力がある時にする処理
        }


        if (Input.GetAxisRaw("GamePad1_Y") > 0)
        {
            //ゲームパッドの移動入力のy軸の正の方向(上方向)にある時にする処理
        }
        if (Input.GetAxisRaw("GamePad1_Y") < 0)
        {
            //ゲームパッドの移動入力のy軸の負の方向(下方向)にある時にする処理
        }

と書いて使ったりします (GamePad1_XとGamePad1_Yは仮想軸入力名) 。

GetAxis関数は使い方はGetAxisRaw関数と同じなのですが、返ってくる値に一部補正が入っています。例えばGetAxisRawでは、仮想入力にキーボードのキーが割り当てられていてそれらが押された場合、1・-1のいずれかの値を返しますが、GetAxis関数の場合は、スムーズに入力の値が増加/減少が行われる様に補正がかかります。これらの補正は以下の一部プロパティに影響されますが、ここでは本旨ではないので触れません。*5

その他の入力を扱う関数についても使い方が多少違いますが、これくらい簡単に使えるので公式リファレンスを確認するなどしてみてください(仮想入力を使うようになったら使用頻度は下がると思いますが…)

ちょっと楽に

慣れている人にはいらないかもしれませんが、そうでない人のために少し簡単にコードを書くヒントを。仮想入力について取得するここまでの関数は引数に文字列を渡してやります。その為、複数人の仮想入力について同じような処理をする時は

        for (int GamePadNum = 1; GamePadNum <= 4; GamePadNum++)
        {
            var ButtonName = "GamePad" + GamePadNum;
            if (Input.GetButtonDown("GamePad" + GamePadNum + "_Jump") == true)
            {
                //それぞれのゲームパッドで特定のボタンが押された時にする処理
            }
        }

例えばこんな感じにしてやるとゲームパッド1~4までについて同じ処理をすることが出来て楽かもしれないです。

(もちろんこのコードはアイデアを示すためのものなのでちゃんと自分で考えて書いてください。例えば…マジックナンバーダメゼッタイ。)

仮想入力の設定

仮想入力のInputManager上の設定

上記の方法でソースコードを書いたとしても、うまく動かないと思います。というのもまだInputManagerの方で設定をしていないからですね。仮想入力の設定の仕方を見ていきましょう。

主要な入力情報の設定

InputManagerのプロパティの説明については以下を参照してください(公式マニュアルより)

プロパティー 説明
Axes 現在のプロジェクトに定義されたすべての入力軸を含みます。「 Size 」は、このプロジェクトでの各入力軸の数で、「 Element 0、1、… 」は、修正する特定の軸になります。
Name ゲームランチャーやスクリプトを通じて軸を参照する文字列。
Descriptive Name ゲームランチャーに表示される「 Positive Button 」関数の詳細な定義。                                                                                  
Descriptive Negative Name ゲームランチャーに表示される「 Negative Button 」関数の詳細な定義。                                                                                  
Negative Button 軸に負の値を送るボタン。
Positive Button 軸に正の値を送るボタン。
Alt Negative Button 軸に負の値を送るための、代わりのボタン。
Alt Positive Button 軸に正の値を送るための、代わりのボタン。
Gravity 入力が再度中心になる速度。「 Type 」が「 key / mouse button 」の場合にのみ使用されます。
Dead 指定した数値未満の正や負の値は 0 として認識されます。ジョイスティックに便利です。
Sensitivity キーボード入力の場合、この値を大きくすると、応答時間が速くなります。値が低いと、より滑らかになります。マウス移動の場合、この値によって実際のマウスの移動差分が縮小拡大されます。
Snap 有効にすると、反対の入力を受け取った直後に、軸の値が 0 にリセットされます。「 Type 」が「 key / mouse button 」の場合にのみ使用されます。
Invert 有効にすると、正のボタンと負のボタンが入れ替わります。(正のボタンが負の値を軸に与え、負のボタンが正の値を軸に与えます。)
Type なんらかのボタンに「 Key / Mouse Button 」、マウスの移動差分やスクロールホイールに「 Mouse Movement 」、アナログジョイスティックに「 Joystick Axis 」やユーザーがウィンドウを揺すった場合に「 Window Movement 」をそれぞれ使用します。
Axis バイスからの入力の軸(ジョイスティック、マウス、ゲームパッドなど)。
Joy Num 使用するジョイスティックの選択。デフォルトでは、すべてのジョイスティックからの入力を回収します。これは、入力軸のみに使用され、ボタンには使用されません。

大体の設定については上の説明で充分かと思います。

キーの名前については以下のようなルールに乗っ取ります(公式マニュアルより)。

  • 通常キー: “a”、“b”、“c” …
  • 数字キー: “1”、“2”、“3”、…
  • 矢印キー: “up”、“down”、“left”、“right”
  • キーパッドキー: “[1]”、“[2]”、“[3]”、“[+]”、“[equals]”
  • 修飾キー: “right shift”、“left shift”、“right ctrl”、“left ctrl”、“right alt”、“left alt”、“right cmd”、“left cmd”
  • マウスボタン: “mouse 0”、“mouse 1”、“mouse 2”、…
  • (任意の)ジョイスティックボタン: “joystick button 0”、“joystick button 1”、“joystick button 2”、…
  • (特定の)ジョイスティックボタン: “joystick 1 button 0”、“joystick 1 button 1”、“joystick 2 button 0”、…
  • 特殊キー: “backspace”、“tab”、“return”、“escape”、“space”、“delete”、“enter”、“insert”、“home”、“end”、“page up”、“page down”
  • Function キー: “f1”、“f2”、“f3”、…

1つ注意が必要なのは、joystickは1からでbuttonは0からということです。

キーボードから仮想軸入力を実現したいのであれば、Positive ButtonもしくはNegative Buttonプロパティに上記のルールに則ってキーを書く事で可能です。

また、

複数の軸を同じ名前で作成することができます。入力軸を取得するとき、もっとも絶対値が大きい軸が返されます。これによりひとつ以上の入力デバイスを軸の名前に割り当てることができます。例えば、ひとつの軸をキーボード入力として作成して、ひとつの軸をジョイスティック入力として同じ名前で作成します。もしユーザーがジョイスティックを使用していれば、入力はジョイスティックから来て、そうでなければキーボードから来ます。これによってどの入力から来ているのかスクリプトを書くときに考慮する必要がありません。

とのこと(公式マニュアル)なので、「ユーザーに環境を強制したくない!」かつ「キーコンフィグでいちいち設定させずとも初期設定でできるようにしておきたい!」という人は、これを使ってゲームパッドとキーボードどちらからでも入力を受け付けるようにしてみるといいかとおもいます。

これで仮想入力の扱い自体は出来ると思います。後は複数の入力を受け付けることが出来ればもう複数人でのゲームを作ることが出来ますね。

参考までにどんな感じの設定にすればいいかの例を載せておきます

  • X軸の仮想入力 f:id:makiofinae:20161216180408p:plain

  • Y軸の仮想入力:Invertにチェックを入れないと入力が反転してしまう事に注意 f:id:makiofinae:20161216180512p:plain

  • ボタンの仮想入力:Positive Buttonにjoystick 【ゲームパッドの番号】 button 【ボタンの番号】という書式で記述することに注意。後述する様にTypeも軸入力とは違う。この場合Axisプロパティは特に意味はない。 f:id:makiofinae:20161216180656p:plain

複数入力

仮想入力を使って複数のゲームパッドからの入力を受け付けるのは、Joy Numプロパティを設定して行います。例えば一つ目のゲームパッドであるゲームパッド1での入力(通常はプレイヤー1の入力として扱う)の入力を見るには、Joy NumをJoyStick 1にします。後は参加を考えているプレイヤーの数だけこの仮想入力を作ってやれば終了です。ここで楽に操作するために、仮想入力は、

  • プレイヤー1の仮想入力
    • X
    • Y
    • Jump
  • プレイヤー2の仮想入力
    • X
    • Y
    • Jump ...

としていくよりも f:id:makiofinae:20161216182013p:plain

  • X
    • プレイヤー1
    • プレイヤー2 ...
  • Y
    • プレイヤー1
    • プレイヤー2 ...
  • Jump
    • プレイヤー1
    • プレイヤー2 ...

f:id:makiofinae:20161216182025p:plain

としていく方が良いかもしれません(伝わって)。

というのも、InputManagerは仮想入力を増やす方法として新規に仮想入力を1から作る複製するかの方法がありますが、1から作るとなるとプロパティについてすべて設定しなければなりません。複製すると、その点プロパティについてはコピーされるので設定の必要がなく、これによってプレイヤー毎に入力の感度等が異なるといった事態を防げます。すると複製をしたいわけですが、複製して作られた仮想入力は、その複製元の仮想入力の下に作られます。そうなると、プレイヤーごとにまとめるよりも、複製がしやすいように入力の種類ごとにまとめたほうが楽です。そうすると、プレイヤーの増加に合わせて仮想入力を増やす時にも他のプレイヤーの仮想入力をDuplicateしてからNameとJoy Numを書き換えるのみで増やすことができます。

もし5人プレイを出来るように仮想入力を増やすことになったら、Duplicateして… f:id:makiofinae:20161216181140p:plain

Nameを変えて… f:id:makiofinae:20161216181225p:plain f:id:makiofinae:20161216181234p:plain

Joy Numも変える!! f:id:makiofinae:20161216181255p:plain f:id:makiofinae:20161216181302p:plain

注意事項

関数を使うにあたって

上のGetButtonDown/GetButton/GetButtonUp関数やGetAxis関数は受け取る仮想入力のTypeが合っていないと正常に動作しません
つまり、GetButtonDown/GetButton/GetButtonUp関数を使う時はそれに渡す仮想入力のTypeプロパティを"Key or Mouse Button"に、GetAxis関数を使う時はそれに渡す仮想入力のTypeプロパティを"Joystick Axis"にしておく必要があります。

入力の検知のタイミング

UnityのUpdate系関数には Update/LateUpdate/FixedUpdateの三種類があります。簡単に説明すると、

  • Updateは御存知の通り毎フレーム呼ばれる関数で、
  • LateUpdateは毎フレームのUpdateが呼ばれた後に呼ばれる関数で、
  • FixedUpdateは固定のインターバル(デフォだと0.02秒)毎に呼ばれる関数です。

それがどうかしたの?って感じかもしれませんがこれについても軽く注意をしておかないと初心者はずっこけます。
まず前提として、仮想入力の管理については、上記関数が呼び出された時に実際に仮想入力があるか等の確認をするのではなく、一定の間隔毎に仮想入力があるか等の確認をして、その情報を保持しておき、それについて上記関数が呼び出されると見にいくような作りになっています。つまり、上記関数は、仮想入力についての情報を取得しているのですが、その情報というのは関数を呼び出したその時の情報ではなく、最後に仮想入力について更新された時の情報について見ているのです。 ではその更新とはいつされるのか、というとUpdate関数が呼ばれる時です。 ある意味メモ化とも言えるのでしょうか?毎フレーム呼んでいたら少し違う気もしますが…

f:id:makiofinae:20161220215755p:plain

つまりは、例え実際に入力がなされたとしても、Update関数が呼ばれるまでは上記関数でその入力については検知できないのです。つまり - 実際の入力中に更新(Update関数)→仮想入力関数の使用 とやれば問題はないのですが - 実際の入力→仮想関数の使用→更新(Update関数) となると、仮想関数は実際の入力について取得することは出来ないのです。 でもそんな場合あるの?毎フレームUpdate関数は呼ばれるし関係なくない?と思うかもしれません。そのUpdate関数が「毎フレーム」呼ばれるというのがミソなのです。毎フレームとはつまりは画面の描画が変更される毎、ということですが、この画面の描画の変更は何も一定で動いているわけではありません。所謂FPSといわれる描画間隔は、処理の重さ等々の事情により変動します。となると、極端な話、1フレーム更新するのに10秒かかることもありえるわけです。そうなると、その10秒の間にボタンを押してから一瞬で離した場合、更新時にボタンは押されていないので入力は検知されません。これについては仮想入力の仕様上避けられないと思うのでそもそも1フレーム10秒もかかるような処理を書かないようにする他ないのですが、これ以外で問題となる場合が出てきます。

それが最初に書いたFixedUpdate関数を使うときなどです。FixedUpdate関数は呼ばれる間隔が一定です。これはどういうことかというと、FixedUpdate関数が1回目呼ばれてから2回目呼ばれるまでの間に、Update関数が2回以上呼ばれることも有り得れば、1回も呼ばれないということも有り得るということです。

f:id:makiofinae:20161220220823p:plain

ここからは言葉で言うよりコードを見たほうが早いかもしれません。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {//画面描画が更新される度に呼ばれる
     //このタイミングで実際には仮想入力情報の更新が行われている
    }

    void FixedUpdate()
    {//固定の時間間隔毎に呼ばれる
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

Numは値を保持するint型の変数です。このように、ボタンが押される度に値をインクリメントする(つもりの)処理をFixedUpdate関数内に書くとします。

ここでGamePad1_Add入力がされ、Update関数が呼ばれて、まさに今 GamePad1_Add入力がなされた、と検知されたとしましょう。そしてその後処理が重くなってFixedUpdate_1FixedUpdate_2FixedUpdate_3Updateという風に呼ばれたとしましょう( FixedUpdate の後ろについている_1等はそれぞれを区別するために便宜的につけたものです)。     そうすると、まず今まさに入力がなされた、という情報が保存されているのでFixedUpdate_1ではNum++;が実行されます。その次の FixedUpdate_2においても、保存された情報の更新がないので今まさに入力がなされたものとしてNum++;が実行されます。 その次の FixedUpdate_3においても同じです。その後Updateにはいって、実際の入力をチェックして、「今まさに入力がなされた」という評価から、「入力が押し下げ状態である」若しくは「入力がなくなった」という評価に、更新されます。

さて、そうするとどうなるか。実際には GamePad1_Add入力は1度しかされていないにも関わらず、Numは3増加してしまいました。これは意図しない動作ですね。

この例とは逆に FixedUpdate関数が1回目呼ばれてから2回目呼ばれるまでの間に、Update関数が複数回呼ばれることもあります。この場合「今まさに入力がなされた」という評価がなされた状態でFixedUpdate関数に至らず、Numが増加しません。これも意図しない動作ですね。

つまり、FixedUpdate関数内に入力判定の処理を書くと意図しない動作をすることがある*6ということがわかります。

f:id:makiofinae:20161220232952p:plain 表にしたほうがわかりにくいのではという良い例(悪い)

…ここまでだと「いや、言われなくてもしねーよ」と思うかもしれません。どこでしてしまう可能性があるかというと、当たり判定を見た上で入力を見るような場合です。

Collision系の物理演算はFixedUpdateと同タイミングで呼び出されて更新されます。つまり仮想入力の更新のタイミング(毎フレーム)と物理演算の更新のタイミング(固定インターバル毎)が違うというわけですね。そうすると、さっきの場合とは反対に物理演算については更新がFixedupdateでされる為、物理演算に関する処理についてはUpdate関数に書くのではなく、FixedUpdate関数内に書くべきであるということです。これはさっきの仮想入力と同じ理屈ですね。呼び出し間隔の異なる2つの関数内のどちらかで更新される情報を取得する関数について、もう一方の関数内で扱うというような使い方をしていると意図した動作をしない可能性があるということです。

そうなると困ったことが出てきます。ここまでの話をまとめると、Input(仮想入力)に関する処理はUpdate関数内に書くべきで、物理演算に関する処理はFixedUpdate関数内に書くべきということなのですが、もし「入力と物理演算について同時に見たい」となったらどうしたら良いのでしょうか?例えばオブジェクト同士が重なっている時に決定キーを押すとスタート、と言った処理や、ボタンを押されている間に接触したオブジェクトを見てそれを破壊する、といった場合などです。これはもう少し実際の製作に近いところから言うと、物理挙動イベント関数(OnCollisionEnter、OnCollisionStay、OnCollisionExit、OnTriggerEnter、OnTriggerStay、OnTriggerExitなど)内で入力を見る処理を書くと、物理挙動イベント関数がFixedUpdate関数で更新されるので上記問題が発生し、意図した動作をしない可能性があるということです。

これが正しい解決策かはわからないのですが、僕は一つの方法としてbool型の変数を作ってそこを経由するという方法を使っています。 まず、上でさっき問題があると言ったコードをもう一度見てみましょう。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {
        //このタイミングで実際には仮想入力情報の更新が行われている
    }

    void FixedUpdate()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

このコードは入力についてFixedUpdate内で呼び出しているのが問題でした。というわけで入力周りの処理をUpdate関数に移しましょう

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

    void FixedUpdate()
    {

    }

では、何かと重なった状態の時にボタンが押されるたび値を増加させるとして、 OnTriggerStay関数を追加してみましょう。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
        }
    }

    void FixedUpdate()
    {

    }

    void OnTriggerStay(Collider other)
    {

    }

さて、追加しましたが、この OnTriggerStay関数はFixedUpdateと同じく等間隔で呼び出されるので、この中に直接仮想入力についての処理を入れるわけにはいきません。そこでbool型の変数を使って状態だけ保持してみます。

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    bool IsOnStay = false;
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true && IsOnStay == true)
        {
            //ゲームパッドのジャンプに使う仮想ボタンが押しこまれた瞬間にする処理
            Num++;//ボタンが押されるたび値を1増加
            IsOnStay = false;
        }
    }

    void FixedUpdate()
    {

    }

    void OnTriggerStay(Collider other)
    {
        IsOnStay = true;//状態を更新
    }




この例だとbool型の変数で保持するのは当たり判定の方ですが、別に仮想入力の方を保持してもいいと思います。つまり、

    //
    //もちろんどこかのMonoBehaviourを継承したクラスの中
    //
    bool IsButtonDown = false;
    void Update()
    {
        if (Input.GetButtonDown("GamePad1_Add") == true)
        {
            IsButtonDown = true;
        }
    }

    void FixedUpdate()
    {

    }

    void OnTriggerStay(Collider other)
    {
        if (IsButtonDown == true)
        {
            Num++; //ボタンが押されるたび値を1増加

            IsButtonDown = false;
        }
    }

とするということです。ただし、この2つは同じ意味ではないことに注意しないといけません。実際に値を増加するタイミングが、フレーム依存の変化するものか、固定のインターバルによるものかが変わってくるからです。

ただし、この回避法は僕がとりあえず出来るようにと即席で使った方法にすぎないので、もっと良い書き方があると思います。もしかするとこれでも意図しない動作が起きるかもしれません。そのため、利用する前にもっといい方法が無いか考えて、もし思いついたら教えて欲しいです(切実)

Thanks for reading!!

最後に他人任せのようなことを書いて申し訳ないですが、こんな感じで僕の説明は終了です。駄文ゆえに苦労をして読んだ人も多いと思いますがここまで読んでくださったことに感謝します。

最後に後からこれをもう一度見るのも面倒だろうと思うので、まとめをしておきます。忘れた時に見返すのにでも使ってもらえると幸いです。

まとめ

  • 特定のキーそのもので入力を検知するのではなく、仮想入力を使おう
    • 仮想入力には仮想軸入力と仮想ボタン入力がある
  • InputManagerは「 Edit->Project Settings->Input 」で開ける
    • 仮想入力の値の設定、新規追加、複製、削除ができる
    • JoyNumと名前の異なる仮想入力を作ることで複数の入力に対応できる
  • 仮想入力について取得する関数は GetButtonDown/GetButton/GetButtonUp関数やGetAxis関数がある
    • InputManagerで仮想入力のプロパティのTypeがそれぞれの関数に対応するものになっていないと使えない
  • Inputが更新されるタイミングに気をつけよう
    • 入力はUpdateと同じタイミングで更新される
    • FixedUpdateに処理を書くのはやめよう
    • 物理挙動イベント関数についてはFixedUpdateと同じく等間隔に更新されることに気をつけよう
      - 物理挙動イベントとInputを同時に使いたいのであれば、bool型の変数を使うと良いかも?
      

おまけ

応用的なもの

初心者にはちょっと難しいかな?と思って触れなかったものについてリンクだけ張っておきます。興味があったらどうぞ!

InputManagerにスクリプトからアクセス

まあこれはしたいと思うので…参考までに

InputManagerを4人分手作業で設定していくのはまずありえないと思ったので

わかる(白目)

参考リンク

Unity公式ページ

一般的なゲーム入力

Input

InputManager

Qiita

UnityのInputで入力を扱う

個人ブログ

Unityで複数コントローラー

【理解した!】スマホ向けUnity スクリプト実行順の謎

Unity 〜UpdateとFixedUpdateの違い〜

Update()とFixedUpdateの使い分け [Unity]

他にもあった気がするけどメモし忘れた…

*1:もちろん多人数プレイのゲームにすれば必ずしも簡単に面白いゲームを作れるというわけではないですが…

*2:本当に楽になるかはさておき

*3:唐突な宣伝

*4:if内の==trueは初心者にわかりやすいように書いただけのものなので特に気にしないでください

*5:気になる人はGravityやSensitivityの値をいじってみると良いかも

*6:実はもっと広くFixedUpdate関数内に物理判定に関係する処理以外の処理を書くと意図しない動作をすることがあるといえます