稲枝の押入れ

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

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

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

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

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

system("pause");

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

つまり、

system("pause");

と書くと、pauseコマンド(任意のキーを押すまで処理を中断するコマンド)が実行される。

これをプログラムの最後においておけば、処理が中断されてプログラムが終わらないのでウィンドウが消えなくなる(勿論キーを押すと終わってしまう)。

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

入力を待つ

CもしくはC++を使っている場合の方法その2。

Cなら

int hoge;
scanf("%d",&hoge);

C++なら

int hoge;
std::cin>>hoge;

などとして適当な変数に入力を入れるようにする。C言語なら<stdio.h>C++なら<iostream>のインクルードが必要。

ブレークポイント

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

ブレークポイントはコードの止めたい位置の左端のあたりをクリックするか、F9キーを押せば設定/解除が出来るはず。

ただし、ブレークポイントで止めた場合はプログラムの画面より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関数内に物理判定に関係する処理以外の処理を書くと意図しない動作をすることがあるといえます

知識というものや学問というものに対する姿勢

突然だが、僕は学問を修めている人を尊敬している。その知識には経済的にも非常に価値があると考えているし、それを大いに使ってくれると良いなと考えている。もっと言うならば、学問とはそれを1人の人間の中に入れることで有機的に結びついて、情報としては本に書かれているものと同じだとしても、実用性からしてその価値は全く異なるものだと考えている。つまり、学問はそれを理解する人が文書に残した物よりも、学問を理解している人の方がよっぽど高価値なものだと思っている。

殴り書きで文章を書いていくので自分の言いたいことが言えるかわからないがそこはご容赦頂きたい。そもそも上の前置きも必要なのかよくわからない。

言いたいことは実はかなり沢山あるのだが、今回はそのうちの1つとしてこれら学問やその要素をなす知識についての姿勢について少し思う事があったのでその話をしたい。

最初に念を押しておきたいのだが、当然ながらこれは僕の思考だ。僕は自分でも認識するくらいに学問至上主義的なところがある。その点について相容れないかも知れない。 また、僕は「こうあるべきではないか」という意図でこれから論を進めていくことになるが、これは別に「こうしろ」だとか「こうでないからお前はダメだ」と言うものではない。ここから書かれるのは、僕が僕の思考で物事を考えた結果導かれる帰結だ。それは僕の思考で物事を考えるという過程を踏んでいるのだから当然にあなたのものと完全に同一であることなどはないだろう。 ただ、これらの違いや差を認識した上で思考をするということが大切なのじゃないかと僕は思っている(メタいようだがこれもあくまで僕が思っているだけだ)。だからこそ、僕は最近良く他人の発言を見て思うところについて自らも思考して発信してみようかと思い至った。

まずそもそも僕が一番感じているのは、学問に対する尊敬が無いのではないかということだ。これだとかなり精神論チックになってしまうので、もう少し掘り下げよう。僕が学問に対する尊敬が無いと言っているのは、学問やそれを修めることを軽視し過ぎではないかということだ。

最近はインターネットで調べられない事は少なくなってきた。スマホの普及でこれが更に押し進められているように感じる。それにともなって、知識というものの持つ価値を感じなくなってきているのではないだろうか。わからないことがあれば調べれば知ることが出来る。そういう環境におかれて、知識というものが当たり前になっている。

こういった背景のもとで一部の知識について適当な発言が飛び出すようになった。ネットを学者がよく使っている界隈は問題ないのだが、専門家が実務で忙しくネットを見ている暇がなかったりすると、その界隈の知識について適当な発言を訂正する人がいない。よってそういった発言が拡散力の高いネットで繰り返される、という形を取っている。まだ調べて得た知識ならマシな方で、よく知りもしないが自分の"常識"から考えてこうだろうという考えを学問の威光を背に声高に叫ぶケースさえある。

これは危惧すべき事だろう。誤った知識は拡散するとその訂正が非常に困難になる。誤った知識を発信した人に解説をして、だから違うよといって納得してもらったところで、その人の発信を見た言うなれば受信者はその受け取った誤った情報を元に何か行動をしてしまい、害を受けたり与えたりするかもしれない。その害は経済的なものから、社会的、身体的なものにまで及び得るだろう。発信者が誤った知識について訂正する発信をしたとしても、その発信が届くかはわからない。人は正しいものではなく、印象に残るものを見ているからだ。そして、人にとって印象に残るものとは最初の情報だ。最初の情報が入ってしまったら後の修正情報については見えなくなってしまう。若しくは、見えていても初めに知ったからという理由だけで、特に根拠もなく自分の持っている情報が正しいと思い込み、修正情報をはねのけてしまうのだ。

具体的に僕がよく目にするのが(若しくは僕の性質上目につくのが、かもしれないが)、法律に関するものだ。ネットで条文を探してきて呟いていたりするのだが、大抵がトンデモ解釈で、その他の条文や判例学説との関係を無視した凄いものだった。法律の運用がどうなされているのかという前提知識さえない様なものも多く、これで誤解して誰かが捕まったりでもしたらどうするんだろうな、と思って見ていた。

法律はそれに関して幾つか学問として成立しているが、特に実定法学を学んでいて思うのが、法学というのは一部の理解というものはほぼありえず、必ず全体を総括的に理解した上で、その中での位置づけとしての一部の理解が必要な学問であると思う。それは法学が裁判等の実務で使われる学問であり、現実の複雑さに対応するために様々な視点を組み込むことで妥当な結論を導こうとした試行錯誤の証でもあると思う。そういう訳で、局地的な理解というのが非常に困難かつ無意味になり得る学問なのだ。また、正確でない知識について一切の意味を持たない学問でもあると思う。これは恣意的な判断を避けるためと予見可能性を担保するためかと思われるが、正直なところ断言はできない。僕は大学で法学を専攻しているが、僕はその一部しか学べていないと思っているので、先程述べた全体からその位置づけとしての一部の理解が必要な学問であるという特質上、あれこれ語ったところでそれが正しいと言い切れるだけの根拠も自信もないのだ。そしてその上で、半端な知識で誤った情報を事実のように発信しているのを見るとやるせなくもなる。この学問はそういう扱いをしてもいいと認識されているのだろうか、と。

実際に、誤った情報はそれを見た人を誤解させるが、その全員の誤解を解くのは難しい、という上記の話を、誤った情報を拡散している人に1度したことがあったが、何も変わることはなかった。自分の中で勝手に間違った解釈をするのであれば好きにしてもらって構わないが、多人数に発信もしくは拡散するとなると前述の通りその意味は大きく変わると思うのであまり適当な解釈をしている発言等を拡散してほしくは無いのだが、まあそこは個々人の考え方なのだろう、僕も強制は出来ない。

ともあれ僕はこのような学問や知識に対する態度に非常に危機感に近い何かを感じている。 「ネットで調べてサッとわかるようなら学問としてそれを未だ研究している人達がいるというのはおかしいのではないか」という当然に行き着きそうな思考が出来ていない、というのがそのうちの1つだ。もし本当にネットの知識を少し読んだだけでその学問分野若しくは知識について習熟したのだとしたら、是非その分野の専門家になっていただきたいし、そうでないなら正確かわからない知識をさも事実のように拡散するのをやめて欲しい。

ここで注意しておきたいのは僕が「ネットで知った知識」という点よりも「軽く調べただけの知識」である点について力点をおいているということだ。ネットにはさっきも言ったように調べられないことが少なくなってきたくらいのたくさんの情報が溢れている。その情報達の中から正しいものを選び出し、全てを拾い切って理解したのだとすればそれは1つの"正しい知識"足り得るだろう。問題は知識が浅く狭いところにあるのだ。

また、この流れで言うと、ネットの知識を過信し過ぎであるように思う。これは別に「ネットはダメだ、紙をめくっていって覚えたことにこそ意味がある」と言ったエセ科学のようなことが言いたいのではなくて、その信用性の話だ。 ネットに沢山の情報があるのは、誰もがそこに書くことが出来るからで(この記事が証拠だ)、つまりは全く理解していない人間が適当な知識を書くことも出来るということだ(この記事が証拠でないことを願いたい)。それを書いた人の情報についてはその本人の自己申告を信じる他無いし、嘘を書いていてもそれを責められない。文章に責任を求められないということは、その正確性を担保しないということでもあると思う。そうなると情報の生後や取捨選択は読み手の能力に委ねられるが、これは非常に難しい。正直言って専門分野の情報なんかどれが正しくてどれが間違っているかなんてわかりやしない。 それに対して書籍(出版に際して書籍と同等の手続が踏まれているなら紙にこだわらず電子書籍でもよい)は、誰もが出版できるわけではない。印刷代や営業広告代等のコストがかかるので、売れる見込みのあるものについてのみ出版されるし、出版社のイメージを損ねぬよう、その道の権威である先生なんかに執筆を頼む。全く理解していない人間が学術書を執筆する機会なんてものはネットの場合に比べて明らかにすくなくなる。また文責が定められていたり、どこの何という何を専門とする学者が書いたのかもわかるので、その正確性が一定程度担保される。 このように比較すると、ネットの情報というのは非常に扱いの難しいものであることがわかる。もちろんネットにある情報の全部が間違っているわけではないし、僕個人が見る分には正しいことのほうが多いと思う。しかし、それが正しいのかはわからないのでしっかり見定めなければならない。しかし、それもないままに学問や知識を扱っているのをたまに見かける。これはとても危険だと思う。

みんなが学問や知識について相応の敬意を払い、フリーのネットの情報の難しさを理解し、不正確な情報を事実として拡散することの危険性を認識してくれたら、おそらく僕にとっては住みやすくなるんだろうな、と思う。

Discordとかいう通話ソフト

って皆さん知ってます?僕はついこの間まで知らなかったんですが、所謂通話アプリです。どうもゲームをしながら通話することを想定して作られたソフトらしいのですが、これがなかなかいいので僕が分かる範囲で紹介したいと思います。僕が分かる範囲で、というのは聞いた話+使ってみた感想という意味です。正確な情報はググったほうが出てくると思うので、話半分で読んでもらえたらと思います。

伝わらないのを恐れず言いますと、SkypeとSlackを足して2で割ったような、そんな感じのソフトウェアになっています。フリーソフトなのですが、なかなかにスタイリッシュなデザインで見た目も悪くありません。手っ取り早くメリットについて話すのも良いのですが、その前にこのDiscordの取っているシステムがどんな感じなのかを軽く説明しておきたいと思います。

サーバー

DiscordではDMと呼ばれる直接フレンド登録したユーザーと通話やチャットをする方法の他に、サーバーを立ててそこで通話やチャットをするという方法があります。このサーバーとは他の通話ソフト等に言うチャットルームや通話グループといった様な括りよりも少し大きい概念で、そのサーバーの中にテキストチャンネルと呼ばれるチャットルームと、ボイスチャンネルという通話グループが内包されています(このチャンネルという呼び方はまさにSlackっぽいですね)。ユーザーの1人がこのサーバーを作成し、そこに他の人を呼ぶことでみんなでそのサーバー内でチャットや通話ができるようになるわけです。そして面白いのが、サーバーの中にはテキストチャンネル若しくはボイスチャンネルを複数作ることも出来るという所です。 例えば、ポケモンをする人を呼ぶためのサーバーを作り、

  • ストーリーについて話し合うテキストチャンネル
  • 攻略法について教え合うテキストチャンネル
  • 対戦相手を探すテキストチャンネル
  • その他雑談するテキストチャンネル

と別々にテキストチャンネルを作って、自分が必要なテキストチャンネルについてのみ読んだり、雑談で重要な情報が埋もれたりするのを防ぐことなんかが出来ます。チャンネル毎のミュートも出来るそうなのでネタバレを防ぐために特定のチャンネルについてはミュートにしておく、なんてことも出来るかもしれませんね ボイスチャンネルについても同じようなことが可能で、例えば多人数プレイのFPSをするとして、

  • チームA,第1分隊のボイスチャンネル
  • チームA,第2分隊のボイスチャンネル
  • チームB,第1分隊のボイスチャンネル
  • チームB,第2分隊のボイスチャンネル

などとすることで、チームが分かれてしまってもそれぞれのプレイヤーが自らのボイスチャンネルに入ることで互いの情報が筒抜けになることがなく、かつ同じチームの別の分隊の指示と自分の分隊の指示とが混同することがなくなります。また、ボイスチャンネル間の移動は非常に簡単なので、ちょっとした用事の際には少し別のボイスチャンネルに移ることも可能です。

さて、ではサーバーの話を具体的にするのはこれくらいにして、少し使ってみて思った良い点悪い点について列挙してみようかと思います。

良い点

  • 上記サーバーとチャンネルの機能が使いやすい
  • ソフトを落とさなくてもブラウザ上でも一応使える
  • Skypeに比べて軽いらしい(友人談)
  • 複数通話時の音声について、その通話相手毎に音量が設定出来る。その為音量の調整が楽。
  • Skypeと違い、グループ通話のマスターが通話を終了することによってグループ通話自体が終了する、ということがない。あくまで通話はサーバー上にあって、その上で通話しているので。サーバー作成者は権限があるだけで通話をやめたらボイスチャンネルが閉じられたりはしない
  • デフォルトで話していない時は相手に音が聞こえない設定になっていて、雑音が入りにくい
  • 使い始めるのが非常に簡単。自分で決めたIDの他何も必要ない。メールアドレスやパスワードまでも最初は不要。
  • Skypeにはない、スピーカーのミュート機能が付いている。
  • TwitchやSteamとの連携が出来る
  • ゲームプレイ中の仕様を想定しているため、ゲーム関係の色んなサービスに意外と対応している
  • サーバー設定により、一定時間話さなかったユーザーをボイスチャンネルから退出させたりといった様々なことが出来る
  • 「s/置換前/置換後」等のコマンドが使用可能
  • Skypeと違い、画像等のファイルをチャットに上げても上げた本人がいないとダウンロードできない、若しくは時間が経ったらダウンロードできないということがない
  • botなどのカスタマイズが可能
  • ユーザごとに権限を付与して管理が可能
    • たとえば、管理者とそれ以外、ではなく管理者・テキストチャンネル権利所持者・ボイスチャンネル権利所持者・一般使用者、といった感じで権限を付与できる
  • テキストチャンネルにそのチャンネルの説明を書いておける
  • 重要な発言についてはピン止め機能がある
  • チャンネルをまとめて「グループ」を作る機能がある
    • 例えば同じサークルの人が集まったサークルで、真面目なサークル運営のことを話し合うチャンネルとみんなでゲームを遊ぶチャンネルがあったときは、ゲームを遊ぶチャンネル類を「game」と言うチャンネルを作ってまとめておける

悪い点

  • Skypeと違い、画面の共有機能は無い
    • DMでであれば、複数人でも画面共有が出来るようになりました。
  • 通話を切る時は受話器を下ろすようなアイコンをクリックするが、通話に参加するときは参加するボイスチャンネルの名前をクリックするしか方法がなく、わかりにくい
  • Skypeと違い通話終了時にその旨や通話時間等が出ない
  • これは出来るソフトのほうが少ないが、特定のサーバーから別のサーバーへ招待する時に、リスト形式で選択して決定をすると選択したユーザーが招待される、の様な機能がない。ユーザーの多数が同じ別のサーバーを作る際に面倒。
    • ユーザーが同じチャンネルに招待コードを貼ればいいだけ。ただし、特定の誰かは呼びたくないと言った場合はこれは使えない。
  • フレンドの登録にDiscordタグと#以下の番号(要は左下にある自分のID的なものと番号の両方)を入れないといけないのだが、説明が書いてはあるものの簡素で初めてだとわかりにくい
  • サーバーへの招待の方法が参加するためのURLを教えてそこから参加してもらうしか無い。そのためSkypeの様な強制追加は出来ない
    • 寧ろ強制追加をする必要はなさそう(過去ログも見えるので)
  • 上記URLの有効時間が15分間だけ
    • URLの有効時間は無効に出来ました

こんな感じです。要確認とあるものについては話を聞いただけなのでよくわからないところです。参考情報までに載せておきましたが、分かり次第更新するつもりです。

まとめ

今日初めて入れてみて数時間友人達とゲームをしながら使ってみたのですが、変な挙動をすることもなく快適に通話をすることが出来ました。操作も割りと直感的に使えたので好感触でした。

加えて、新しくDiscordを使う時に必要な情報が使いたいユーザーIDだけ、というのが印象的でした。ユーザーIDのみで仮登録できて、通話等も出来るのですが、続けて使いたい場合(?)にはメールアドレスとパスワードを登録することで本登録となります。このシステム、少し試してみたいだけでもいちいち幾つか情報を登録してやらないといけないサービスが多い中、非常にいいものだと思います。このシステムがもっと一般化されたら、新しいソフトウェアを試す事に対するハードルが下がってユーザーは嬉しく、最終的にはソフトウェア業界的も試してもらえて嬉しいという関係になれるのかな、と思ったりしました。

そんなこんなで、最近Skype重いなあ…なんて思っている人には特にオススメです。導入も非常に簡単なのがDiscordのいいところだと思うので是非試してみてはどうでしょうか。