SionGames 技術Blog

C#やUnityを触っているサーバープログラマーが色々と書いていく所

VMWareとCentOS7をインストールする手順

自分はサーバーエンジニアなのでLinuxによく触れる機会があり、検証のために仮想環境のLinuxを新規で作る事も多いです。
そこで手順書も兼ねてWindowsマシン上にCentOS7の環境構築を行う方法を記事にしてみようと思います。

CentOS7をダウンロード


CentOSのISOイメージのダウンロードには少々時間がかかりますので、先に下記URLからダウンロードしておきます。
https://www.centos.org/download/

f:id:SionGames:20190816200023p:plain
DVDISOをクリック

VMWareをインストール


これは商用で無ければ無料で仮想環境を作ることが出来るとても便利なソフトです。
今回はCentOS7を入れるために使用しますが、OSを用意出来ればWindowsの中にもう一つWindowsを立ち上げる事も可能です。
既にVMWareがインストールされている場合はこの手順はスキップして下さい。

まずは下記URLからVMware Workstation Playerを探して下さい。
Windows用のVMWareをダウンロードします。
今回は VMware Workstation 15.1.0 Player for Windows 64-bit Operating Systems をダウンロードしました。
https://my.vmware.com/jp/web/vmware/downloads

f:id:SionGames:20190816131203p:plain
VMWare Workstation Player を見つけて製品のダウンロードをクリック

f:id:SionGames:20190816131452p:plain
Windows用のVMWareをダウンロード

ダウンロードしてきたインストーラーを起動します。
特に修正することなく「次へ」をクリックして、最後に「インストール」をクリックします。
インストールが終わるとセットアップウィザードの「完了」ボタンが出てきますのでクリックします。

初回起動時はライセンスキー入力の画面が出てきますが、商用利用で無い場合や、仕事場での検証用途の場合はライセンスは不要です。

f:id:SionGames:20190816131818p:plain
初回起動時の画面

うまく起動できると仮想マシン管理の画面が出ると思います。
ちなみに自分はVMWareWorkstation12から15に更新してみましたが、特に問題なく旧VMWareで作成したCentOS7仮想マシンの起動が出来ました。

CentOS7 の仮想環境を作成する


VMWareとCentOSのISOイメージが手元に揃ったら、VMWare上で仮想マシンを作成します。

VMWareを起動して「新規仮想マシンの作成」ボタンをクリックします。

すると新規仮想マシンウィザード画面が立ち上がります。
インストーラーディスクイメージファイルにチェックを入れた後、先ほどダウンロードしてきたISOイメージを指定します。

イメージを指定したら「次へ」をクリックして画面を進めます。
名前をわかりやすい名前で指定した後、仮想マシンの保存場所を指定します。
指定出来たら再度「次へ」をクリックして下さい。

ディスクの容量を指定する画面が出ると思います。
用途に合わせた容量を各自で指定後に、仮想ディスクを複数のファイルに分割にチェックが入っていることを確認して下さい。
確認できたら「次へ」をクリックします。

f:id:SionGames:20190817002252p:plain

ここまでで仮想マシンを作成する準備が完了できたと思います。
メモリ量やCPUコア数割り当てなどは後から変更も可能です。
「完了」ボタンを押して仮想マシンを作成します。

CentOS7 を仮想環境でインストールする


完了ボタンを押すと仮想環境が立ち上がり、CentOS7のインストール画面が出てくると思います。
「Install CentOS 7」にカーソルを合わせてEnterキーを押します。
下の「Test this media & install CentOS 7」を間違えて選択した場合、メディアチェックが入って無駄に時間がかかりますが特に大きな問題はありません。
仮想マシンからカーソル操作をWindows上に戻すには、Ctrl + Altを同時に押して下さい。

f:id:SionGames:20190817003356p:plain
Install CentOS 7 を選択

しばらく経つとCentOS7のインストール設定画面が出てきます。
インストール時に使用する言語を日本語で探して設定した後に「続行」ボタンをクリックします。

インストールの概要の画面が出てきて、タイムゾーンの設定や初期インストールソフトの選択を行います。
タイムゾーンは何となく設定できると思いますので、東京に合わせて下さい。
CentOS7でGUIインターフェースが欲しい場合は、まずソフトウェアの選択をクリックします。

f:id:SionGames:20190817004541p:plain

ベース環境の選択で、KDE Plasma Workspaces にチェックを入れます。
GNOME Desctopを入れても良いですが、好みに合わせて下さい。
GUIインターフェースが不要であれば、最小限のインストールでも問題ありません。
チェックを入れた後は画面左上の「完了」ボタンをクリックして下さい。

f:id:SionGames:20190817004911p:plain

インストール先の設定で注意のマークが出ていると思うので一度覗きに行きます。
独自にパーティションを別けたりしない場合は特に触らなくて良いと思います。
念のため自分の設定を画像で載せておきます。

f:id:SionGames:20190817010048p:plain

次に、ネットワークとホスト名を選択します。
ホスト名を好みの物に設定して「完了」ボタンをクリックして下さい。

ここまで設定出来たらインストールの概要画面の右下にある
「インストールの開始」ボタンをクリックして下さい。

インストール作業を裏で進めている間にRootパスワードとユーザーを作成する画面が出るので、それぞれ設定して下さい。
インストールが完了したら「再起動」ボタンをクリックして下さい。

再起動完了後は初期セットアップ画面が表示されます。
CentOSのライセンスに同意して「設定の完了」をクリックして下さい。

するとCentOS7のログイン画面が出てくるので、
先ほど設定したユーザー名と対応するパスワードを入力後、
CentOS7にログイン出来たらひとまずインストール作業は終了です。

ただし現状だとWindowsホストマシン側とCentOS7仮想マシン間の通信がまだ出来ないかと思いますので設定して行きます。
画面左下の顔のようなアイコンをクリック後、
インターネットインターフェースを選択して接続して下さい。

f:id:SionGames:20190817020936p:plain

接続が出来たら適当な画面を右クリックして「Konsole」をクリックします。
するとユーザーのホームディレクトリの場所でコンソールが立ち上がりますので、
ifconfig コマンドで仮想マシンに割り当てられたIPアドレスを確認して下さい。
Windows側から仮想マシンのIPアドレスに向けてpingコマンドで応答が返れば通信成功です。

検証が終わったなどで仮想環境が不要になったら、
仮想マシン設定→オプションタブ→全般クリック→ワーキングディレクトリのフォルダをゴミ箱に送るだけできれいさっぱり削除できます。

f:id:SionGames:20190817022021p:plain

仮想マシン設定では仮想環境のスペックなどを弄る事が出来るので興味があったら色々触ってみて下さい。

後書き


これで何時でもCentOS7を立てる事が出来て、Gitを立てて自作アプリのバージョン管理を行ったり、MySQLを立ててデータ保存に利用したりと夢が広がりますね!
今回建てた仮想環境は今後の記事作成にも利用しようと思います。

あと、VMWareは時々脆弱性が発見されて更新する必要が出たりしますので、
時々VMWare本体の更新を確認するようにお願いします。

ゲームプログラマーになる為には? 業界入りした筆者の実体験

何を行えばゲームプログラマになれるのか分からない。
ゲーム業界に転職したい人や学生さん向けに、今ゲーム業界で働いている筆者が学生の頃に何をしていたのか。
勉強内容や就活対策で行ったことなど含めて、学生時代の実体験を記してみたいと思います。

ゲーム業界で働きたいと思った


小学生の頃は家庭用ゲーム機でよく遊び、中学生の頃からはオンラインゲームを中心にかなり遊んでいました。
高校2年に入ってそろそろ進路を決める時期になり、『ゲーム作って働けたら楽しいのかな?』と少しずつ思い始めまして、
高校3年にはゲームを作る事を仕事にしようと方針を決めました。

この時ゲームを作る事を仕事にしたいけれど、実際に何をしたら良いのか右も左も分からない状態でした。
独学などとても出来ないと思っていました。

なので大学か専門学校に入って勉強する選択を取りました。
ただ、ゲームに特化した大学が身近に無かった事や、
業界に入る一番近い方法が専門学校かな…?と調べているうちに感じたので、ゲーム専門学校で就職に強そうな所を探して入学する事にしました。
4年制課程の学科に入りました。2013年の出来事です。

専門学校で何を勉強したか


まず大雑把に専門学校で勉強した内容を書きます。
1年生ではC言語の基礎文法と基本情報技術者試験対策。
2年生ではC++言語の基礎文法とDirectX,Wiiを使った2Dゲーム個人制作、チーム制作。応用情報技術者試験対策。
3年生ではDirectXを使った3Dゲーム制作。就職作品作り。
4年生では企業連携でUnityを使ったゲーム制作やSocket通信、Linux基礎操作、社会人のマナー。

1年生ではC言語とコンソールアプリケーション

プログラムを全く知らない状態から1年間で勉強できた項目を列挙すると

  • printf(), scanf()といった入出力。"%d"の使い方等
  • char, short, int, long型などの基本データ型
  • if文、switch文、for文、while文といった基本文法
  • +, -, *, / などの四則演算。%で割り算の余り
  • 関数の作り方
  • 定数(#define, const)
  • グローバル変数
  • 配列、2次元配列
  • 構造体
  • ポインタ
  • fopen, fclose等のファイル操作

のように並べると色々あります。

他にも四角形の当たり判定の実装方法や弾の飛ばし方などゲーム特有の処理の実装処理を勉強しました。
自分は1年間勉強した結果、コンソールアプリケーションで横スクロールアクションゲームが作れるくらいになりました。
ゲームを作っているうちにノリに乗っていく感覚や自分の目指すゲームに近づいていく感覚がとても楽しく、ゲーム作りは自分に合っていると感じました。
学内コンテストで銀賞を頂き高く評価してもらえた事もあったと思います。

ちなみに学校からはカーソルの位置を変更する処理や背景色・文字色の変更処理、BGM・SEを鳴らす処理の入ったライブラリが提供されていましたが、
カーソル位置と背景文字色を変更するぐらいは意外と簡単に作る事が出来ます。

2,3年生ではC++とDirectX9

C言語を勉強 & ゲームを作って実践を1年繰り返した後に、C++のクラスを使って継承などの勉強に入りました。
再度勉強内容を列挙してみます。

  • 複数cppファイルの使い方
  • 動的メモリー確保 (malloc, new, スマートポインタ)
  • 関数ポインター
  • リスト構造
  • classを使ったオブジェクト指向設計
  • classの派生と継承。protectedの使い方。ポリモーフィズム
  • コピーコンストラクタ
  • namespace
  • 演算子のオーバーロード

徐々に言語機能だけでなくプログラミングでよく使用される基本的なアルゴリズムや設計に関する勉強も入ってきました

そして2年生になってDirectXの授業が入り始めました。
ゲームらしい画面になり始めてかなりテンションが上がったのを覚えています。
最初はDirectX9の固定パイプラインに頂点情報を流して3角形や4角形を表示するところから始まりました。
そこから徐々にテクスチャを貼って画像付きの4角形を出すようにしたり、α情報を入れて透明度を調整出来るようにしたり、加算合成、減算合成を使ってエフェクトを出せるようになったりと、勉強が進むたびに表現の幅も広がっていきました。
ただし、2019年現在ではDirectX9は流石に古いので、DirectX11や12等を主体に勉強した方が良いかもしれません。

2年生後半には3Dのゲームを作れるようz軸を扱うようになりました。
向きを決めるために行列計算なども勉強しました。
メタセコイアやブレンダーを使って3Dモデルを軽く制作する事もしました。
3年生の頃には影を作るための設定やシェーダー周りの勉強が徐々に行われるようになりました。

Unityが学校内で流行

学生内でUnityがゲームが作りやすくて良いらしいと広まり、3年生以降のチーム制作は大半がUnityを使用してゲームを作るようになりました。
言語がC,C++からC#へ変化して当時は戸惑う事もありましたが、同じC系という事もあり似通っている部分が多く比較的早く馴染めました。
そしてこの経験が今職場でUnityを使っているのでかなり活きてます。

ゲーム業界に入るための就職作品作り


就職作品とは

ゲーム業界にプログラマで入るためには作品の提示を求められる事がほとんどです。
書類選考の際に自らの制作したゲームデータを企業に提出するのですが、その際に提出するゲームが就職作品です。
学校では就職作品の制作は大体3年生の夏から冬にかけてが主な時期でした。
過去に制作したゲームを提出しても良いですが、自らの力量を企業に伝えるという事なので、最新の就職作品は気合いを入れて制作する必要があります。
力量を伝えるという関係上、企業にもよりますがUnityで作成した作品よりもDirectXで作成した作品の方が評価されやすいです。

さらに、ゲームを作るだけでは無く、自分が得意とする分野・強みをアピールできると尚良いです。
自分は通信を使ったゲームを作れるというのは十分強みになり得ると教わりましたので、昔から好きだったTPSゲームを作る事にしました。
3Dグラフィックが扱えることを示すためにもTPSは丁度良いジャンルでした。

自分が作成した就職作品と工夫

オンラインTPSを作るという事で、画面出力を行うクライアント側とゲームの重要な判定(当たり判定等)を行うサーバー側の双方を作成する必要がありました。

クライアント側
既に作成していたDirectX9の描画処理をベースにクライアント側の作成を進めました。
モデルを人間にしてスキンメッシュアニメーションにチャレンジしたくなったのでその処理を組むことにしました。
データ構造が簡単なMMDなどに使用されるpmdファイルを解析する所から始めて、なんとかうまく画面にMMDモデルを表示できた時はとても達成感がありました。
そこからvmdのアニメーションファイルを作成してスキンアニメーションまで出来るようになるまでかなり時間がかかりました。

ゲーム作りでは問題が発生するのは付きもので、
スキンアニメーションの頂点位置をCPUで演算すると時間がかかりすぎて重い事が判明しました。
なので頂点座標計算を頂点シェーダー側に移して高速化する事で解決しました。
他にもシェーダーを触ったついでに光弾の見た目をよくするためにピクセルシェーダーにブラーを掛けさせてみたりと、当時の自分に出来た最大限の工夫を凝らしました。

サーバー側
サーバー側ではデータを受け取って当たり判定を行ったり移動を他のクライアントに伝達する処理を作成しました。
ゲーム実行中に位置情報が正しく受信できているか確認したかったので、コンソール画面に随時デバッグ用情報を出力してサーバー内の動作を確認しながら作成を進めました。
オンラインゲーム好きの筆者としてはチャット処理が出来た時がとてもうれしかったです。

そんなこんなで約半年ほど掛けまして、
ローカルネットワーク内の複数PCで弾を撃ち合える状態までゲームを作成出来ました。
ただ、半年では時間が足りず、サーバー選択を兼ねたタイトル画面とゲーム画面のみしか作成できませんでした。

就活


資格は応用情報技術者試験まで取れまして、他はJ検定などで資格欄を埋めました。
ただゲーム系は資格よりも就職作品が重要視されるイメージです。
就職作品を頑張った甲斐があったのか、就職活動を始めた時に書類選考で落ちる事は非常に少なかったです。
ただ自分は面接がかなり苦手でした。
緊張してうまく喋れなかったり、面接的にNGなネガティブな事まで正直に話していて面接が中々通りませんでした。
しかし何度か数をこなすのと自分なりに面接での反省を繰り返し行った結果、無事ゲーム会社の内定がもらえるようになりました。
第一志望で行きたいゲーム会社がある場合は面接に慣れるように対策を打ってから向かうのがお勧めです。

あとがき


学生時代に行ったことをつらつらと書いてみましたが、毎日がとても楽しく、時期によってはとても大変でした。
手を抜く事も出来ましたが、高い学費を払っているのに勿体ない上、自分の首を絞めるだけなので精一杯の努力をしました。
努力をした結果に身についた知識が、回りまわって今の自分を助けていて、無駄では無かったと強く思えます。

学校の良い所は他の学友と意見交換しながら知識の共有が出来る事です。
どうしても理解が出来ない箇所を相談したり、逆に尋ねられた時は教えたりといった事が出来たのはとても良かったと思います。
当時色々と相談に乗ってくれた学友には今でも感謝の気持ちでいっぱいです。

そしてこの記事がゲーム業界を目指す人の参考になれば幸いです。

ゲームで使う円と球の当たり判定

ゲームで当たり判定を行いたい時の手軽な判定方法の一つに
円や球の当たり判定があります。
Unityではその辺りの計算を知らなくてもゲームエンジン側で
当たり判定の仕組みが用意されていますが、
自分で計算を知っておくと役に立つ事もあります。
(サーバー側で当たり判定を実装する必要が出た時とか)

2点間の距離の公式


この公式は今回の円と球の当たり判定の両方に使用する数学の公式になります。
2次元座標系
距離 =  \sqrt{ ( ax-bx ) ^2 + ( ay-by ) ^2 }
3次元座標系
距離 =  \sqrt{ ( ax-bx ) ^2 + ( ay-by ) ^2 + ( az-bz ) ^2 }

C#ソースコード的にはこのようになります。
2次元座標系

float xDis = x2 - x1;
float yDis = y2 - y1;
double distance = Math.sqrt((xDis * xDis) + (yDis * yDis));

3次元座標系

float xDis = x2 - x1;
float yDis = y2 - y1;
float zDis = z2 - z1;
double distance = Math.sqrt((xDis * xDis) + (yDis * yDis) + (zDis * zDis));

円の当たり判定


円の当たり判定は、2つの円の距離を上記の公式で求める必要があります。
円Aの中心座標が(1, 1)
円Bの中心座標が(3, 3)だったとすると
距離 =  \sqrt{ ( 1-3 ) ^2 + ( 1-3 ) ^2 }
距離 =  \sqrt{ 8 }
距離 =  2\sqrt{ 2 }
距離 ≒ 2.828
ということは、円Aの半径+円Bの半径の和が2.828以上であれば
二つの円は重なっていると判定できます。

f:id:SionGames:20190713150743p:plain
半径1の円が(1,1)と(3,3)にあるときの図

球の当たり判定


球の当たり判定は円の当たり判定にz軸を追加するだけです。 球Aの中心座標が(1, 1, 1)
球Bの中心座標が(3, 3, 3)だったとすると
距離 =  \sqrt{ ( 1-3 ) ^2 + ( 1-3 ) ^2 + ( 1-3 ) ^2 }
距離 =  \sqrt{ 12 }
距離 =  3\sqrt{ 2 }
距離 ≒ 3.464
ということで、球Aの半径+球Bの半径の和が3.464以上であれば
二つの球は重なっていると判定できます。

処理負荷軽減


上で説明した方法で当たり判定は実装できるのですが、
Math.sqrt()は少々負荷がかかる処理です。
円や球の距離が不要かつ重なっているかだけ判定したい場合は
Math.sqrt()無しで処理を記述する事が出来ます。

float radius = A半径 + B半径;
float xDis = x2 - x1;
float yDis = y2 - y1;
float distanceSquare = ( xDis * xDis ) + ( yDis * yDis );
bool isHit = ( radius * radius ) > distanceSquare;

処理負荷の高いシステムではこのような最適化も活用したいです。

C#の単体テストの実装方法

バグは必ず出てしまう物である以上、バグを見つけるための手法が必要です。
その手法の一つとして単体テストがあります。
今回は単体テストのメリットとデメリットの説明と、
C#でNUnitを使ったテストコードの実装方法まで紹介したいと思います。

単体テストとは


そもそも単体テストについて馴染みが無い方向けに説明します。
単体テストは作成したプログラムで最も小さい機能毎に検証コードを書いて、小さい機能毎にバグが潜んでいないか確認する事です。
小さい機能を組み合わせた後に行うテストは結合テストと言います。
小さい機能とはC#だと一つのクラスだったり、一つの関数だったりします。

単体テストを行うメリット


メリットはバグを早期に発見しやすくなる事にあります。
後から周辺機能の修正を行った際にも、バグを生み出してしまっていないかある程度指針になります。
大規模で継続的に修正があるプロジェクトであるほど、このメリットは大きなものになります。
プログラムのバグは少なければ少ないほど、顧客の評価を落としにくく出来るのでとても重要なメリットです。
最近のソーシャルゲームの開発状況とピッタリ合います。

単体テストを行うデメリット


デメリットはテストコードを書く工数が増える事と、テストコードの維持にもコストがかかることです。
きっちりとテストコードを書く場合、体感的には1.5倍から2倍程工数が増えます。
そしてテストコードの記述自体に間違いがあった場合やテストが甘かった場合にはバグを発見できない事があります。

疎結合を維持しよう


テストコードを書きやすいコードと書きにくいコードが存在します。

例えば通信や入力などが絡んでいる処理系を密結合で作成している場合です。
通信を行うクラスを直接ロジック側でstaticに参照すると、通信の初期化処理が無いとエラーが出たり 通信相手がいないとエラーが出るのでテストが出来ないという事が起きます。

この場合ロジック側からは通信インターフェースだけを利用する形にするのが良いです。
インターフェースを通すことで通信の実装クラスを切り替えることが出来るので、
テスト用通信実装にも切り替えることが出来るようになります。

こうするとテストコードが書きやすくなるだけでなく、ゲーム制作だと負荷テスト用のBot作成時などにも処理を流用しやすく出来ます。
static変数を使用する事も密結合となり、コードの依存性が高くなるので避けるべきでしょう。
static変数が存在するとテストの並列実行が難しくなります。
これら疎結合を維持する事は保守性の高さや拡張性の高さに繋がりますのでテストコード関係なく意識したい所だと思います。

テストコード実装(NUnit)


では実際にテストコードの実装方法を説明します。 言語はC#です。

NUnit初期導入

まずは適当なソリューションを作成します。
ソリューションを用意した後はテストコード記述用のプロジェクトを立ち上げます。

f:id:SionGames:20190507011718p:plain
テストコード用プロジェクト立ち上げ
もしここでNUnitが存在しない場合はNuGetパッケージからNUnitのインストールを試してみて下さい。
f:id:SionGames:20190507011722p:plain
NuGet
テストコード用のプロジェクトを立ち上げた後、テスト対象プロジェクトを参照に追加して下さい。
f:id:SionGames:20190507011715p:plain
f:id:SionGames:20190507011713p:plain
テスト対象プロジェクトの参照追加

ひとまずこれでNUnitを利用する土台は整いました。

NUnit利用方法

テストコードを追加する度に毎回書くことになるコードの説明です。 実装側の処理を例として書いてみました。

/// <summary>
/// ダメージ計算用ステータス
/// </summary>
public class BattleStatus
{
    public int Attack { get; set; }
    public int Defence { get; set; }
}

/// <summary>
/// ダメージ計算処理
/// </summary>
public class BattleCalc
{
    public int CalcDamage(BattleStatus atk, BattleStatus def)
    {
        int damage = def.Defence = atk.Attack;
        return damage;
    }
}

既にお気付きの方がいるかもしれませんが、ダメージ計算式が間違っています。

テストコード側も載せます。

using NUnit.Framework;

/// <summary>
/// ダメージ計算のテストコード
/// </summary>
public class BattleTest
{
    [SetUp]
    public void Setup()
    {
        //各テストメソッドの開始直前に呼ばれる
    }
    [TearDown]
    public void TearDown()
    {
        //各テストメソッドの終了後に呼ばれる
    }

    [Test]
    public void Test1()
    {
        var calculator = new BattleCalc();
        var atkStatus = new BattleStatus();
        var defStatus = new BattleStatus();

        atkStatus.Attack = 5;

        var damage = calculator.CalcDamage(atkStatus, defStatus);

        Assert.AreEqual(5, damage);
    }

    [TestCase(5, 0, 5)]
    [TestCase(5, 3, 2)]
    public void TestCase1(int attack, int defence, int anserDamage)
    {
        var calculator = new BattleCalc();
        var atkStatus = new BattleStatus();
        var defStatus = new BattleStatus();

        atkStatus.Attack = attack;
        defStatus.Defence = defence;

        var damage = calculator.CalcDamage(atkStatus, defStatus);

        Assert.AreEqual(anserDamage, damage);
    }
}

[SetUp]や[TearDown]のアトリビュートはソース上に書いてある通り、テストの実行直前や直後に呼ばれる関数です。
[Test]や[TestCase]のアトリビュートを付けた関数がテストとして実際に実行されます。
TestCaseを利用するとテストコード側関数の引数に値を入れられるので複数パターンでテストしたい時にとても重宝します。

テストの実行を選択するとテストエクスプローラーに結果が表示されます。

f:id:SionGames:20190507014122p:plain
テストの実行
f:id:SionGames:20190507014455p:plain
テストの結果
結果は御覧の通りで一部のテストが失敗しているのが確認出来ます。
失敗したテストをクリックすると、失敗した場所や理由がStackTrace付きで確認出来ます
また、TestCaseを使用していないテストコードでは検証パターンが不足していて甘いテストコードになっています。
検証パターンが不足している為、今回埋め込まれたバグが検出できずにいる状態です。
出来るだけ考えうる全てのパターンを網羅できると良いと思います。

今回検証チェックにはAssert.AreEqual()関数を使用していますが、これは同じ値でなければ例外を出してテストを失敗させる機能です。
他にもAreNotEqual()関数やGreater()関数など様々な検証用関数があるので調べて見て下さい。

後書き


テストコードは作ったら終わりではなく、継続的に実行と維持を行う事が重要です。
Gitでマージされたあとのタイミングなどで自動テスト実行を行い、失敗時に通知させる環境を整えてしまえばバグが早期に発見できて効果的です。
余裕があれば自動テスト実行を行う環境構築なども記事にしてみたいと思います。

C#のマルチスレッド処理でのロック制御

C#なゲームサーバーアプリケーションを扱っているとプレイヤーデータがマルチスレッドにアクセスされる事が良くあります。
そこで必ず必要となるのがロック絡みのお話。
自分の忘備録も兼ねて様々なロック制御方法について紹介したいと思います。

マルチスレッドとロックについて説明


そもそもマルチスレッドとかロックについてよく知らない方向けにマルチスレッドとロックの関係について説明します。
スレッドAとスレッドBで同一変数に対して操作を行うと後から実行された方のスレッドに結果が上書きされて値がおかしくなることがあります。

f:id:SionGames:20190408012327p:plain
lock制御が無いと変数valueがおかしくなる図

上記の図の例だとスレッドAの結果が無かったことにされてしまっています。
ロック制御を行うとスレッドAの処理の終了を待ってからスレッドBの処理を開始するという具合で排他制御を行うことが出来ます。

様々なロック制御方法


C#でロック制御を行う際の処理を実際のソースコードを使用しながら解説します
コードの実行環境は.Net Core2.1のC#コンソールアプリケーションになります。
ProcessBaseクラスは記事の最後に全コードを載せますが、
ひとまず簡易に必要な所だけ記載しています。

public abstract class ProcessBase
{
    protected volatile int value = 0;
    //以下記事の最後に掲載しているので省略。
}

1. Lock無し(正しくない実装)

まずはロックを行わない正しくない実装から入ります。
AddProcess関数が複数スレッド上で実行されると、
変数valueがおかしな数値になります。

/// <summary>
/// ロック制御をしない(値がおかしくなる実装)
/// </summary>
public class NoLockProcess : ProcessBase
{
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            value++;
        }
    }
}

2. InterLocked

変数単体の排他制御で良ければこのクラスを利用しましょう。
最も高速に動作する排他制御です。
ただしInterlocked.Add()などはInt型とlong型でのみ利用可能です

メリット デメリット
最も高速な排他制御 変数の書き換え用途のみ
お手軽 演算の排他制御はint,long型のみ
2つ以上の処理を同一Lockに出来ない
/// <summary>
/// InterLockedクラスを使用して排他制御する
/// </summary>
public class InterLockedProcess : ProcessBase
{
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            Interlocked.Increment(ref value);
        }
    }
}

3. lock構文

簡単に利用できる排他制御です。
lockの{}に包まれた範囲内で排他制御ができます。
デメリットは何か書こうと思っても思いつきませんでした。

メリット デメリット
高速な排他制御
お手軽
簡単
/// <summary>
/// lock()構文を使用して排他制御する
/// </summary>
public class LockProcess : ProcessBase
{
    private object lockObj = new object();
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            lock (lockObj)
            {
                value++;
            }
        }
    }
}

4. ReaderWriterLockSlim

書き込み処理のみを排他制御させつつ、読み込み処理を出来るだけ並列に動作可能にするクラスです。
ゲームサーバーでは読み込みが並列実行できるメリットが大きく、相性が良いのでよく利用されています。
UpgradeableReadLockは後で書き換えが発生する事がある読み込み処理で利用します。

メリット デメリット
高速な排他制御 ラッパークラスを用意しないと使用するのが手間
読み込みが並列実行可
/// <summary>
/// ReaderWriterLockSlimを使用して排他制御する。
/// ReadLock同士は重複して実行可能。読み込み処理だけに使用できる
/// UpgradeableReadLock同士は重複できないが、ReadLockは重複可能。WriteLockに昇格可能。書き換え前の読み込み処理に使用できる。
/// WriteLockは全てのロックモードで重複不可。書き換え発生時に使用する。
/// </summary>
public class ReaderWriterLockSlimProcess : ProcessBase
{
    private ReaderWriterLockSlim lockObj = new ReaderWriterLockSlim();
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            lockObj.EnterWriteLock();
            try
            {
                value++;
            }
            finally
            {
                lockObj.ExitWriteLock();
            }
        }
    }
}

try finallyを書くのが面倒なのでusing句で囲って使用可能にすると便利です。

/// <summary>
/// ReaderWriterLockSlimのUpgradeableReadLockをUsing句で楽に使えるようにしたもの
/// </summary>
public struct WriteLock : IDisposable
{
    ReaderWriterLockSlim obj;
    public WriteLock(ReaderWriterLockSlim lockObj)
    {
        obj = lockObj;
        lockObj.EnterWriteLock();
    }
    public void Dispose()
    {
        obj.ExitWriteLock();
    }
}

/// <summary>
/// ReaderWriteLockSlimをusing句で簡単に利用できるようパッキングを施した物
/// </summary>
public class ReaderWriterLockSlimProcess2 : ProcessBase
{
    private ReaderWriterLockSlim lockObj = new ReaderWriterLockSlim();
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            using(var wl = new WriteLock(lockObj))
            {
                value++;
            }
        }
    }
}

5. SemaphoreSlim

ロック取得スレッドとロック開放スレッドが異なっていても使用可能です。
つまりロック中にasync/awaitが存在しても使用可能な排他制御です。

メリット デメリット
ロック中にasync/awaitが使用可能 ラッパークラスを用意しないと使用するのが手間
/// <summary>
/// SemaphoreSlimを使用して排他制御する
/// ロック中にasync/awaitが使用できる。つまりロック開始スレッドとロック終了スレッドが別でも良いのが特徴
/// </summary>
public class SemaphoreSlimProcess : ProcessBase
{
    private SemaphoreSlim lockObj = new SemaphoreSlim(1, 1);
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            lockObj.Wait();
            try
            {
                value++;
            }
            finally
            {
                lockObj.Release();
            }

        }
    }
}

6. MutexProcess

プロセス間で利用可能な排他制御です。
ただ非常に重い排他制御です。

メリット デメリット
プロセス間で排他処理ができる ラッパークラスを用意しないと使用するのが手間
非常に重い
public class MutexProcess : ProcessBase
{
    private Mutex lockObj = new Mutex();
    public override void AddProcess(int loopCount)
    {
        for (int i = 0; i < loopCount; ++i)
        {
            lockObj.WaitOne();
            try
            {
                value++;
            }
            finally
            {
                lockObj.ReleaseMutex();
            }

        }
    }
}

各ロック処理の時間計測結果


詳細なソースコードは記事の最後に載せています。
Core i7 6700HQ(定格2.6GHz, 計測時大体3.1GHz)なマシンで
100万回の加算を4スレッドで分割して回した結果を10回平均したものです。

ロック機構名 100万回の処理時間(ms)
NoLock 9ms
InterLocked 19ms
lock構文 56ms
ReaderWriterLockSlim 43ms
ReaderWriterLockSlim usingVer 50ms
SemaphoreSlim 113ms
Mutex 5077ms

予想よりReaderWriterLockSlimが早かったです。

検証コード全文


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace LockTest
{
    class Program
    {
        const int ThreadNum = 4;
        //Execute実行後のvalue値
        const int TargetValue = 1000000;
        const int ExecuteCount = 10;

        static void Main(string[] args)
        {
            Console.WriteLine("Main 開始");

            //スレッドプールのスレッド数を固定しておく
            ThreadPool.SetMinThreads(ThreadNum, ThreadNum);
            ThreadPool.SetMaxThreads(ThreadNum, ThreadNum);

            var processList = new List<ProcessBase>();

            processList.Add(new NoLockProcess());
            processList.Add(new InterLockedProcess());
            processList.Add(new LockProcess());
            processList.Add(new ReaderWriterLockSlimProcess());
            processList.Add(new SemaphoreSlimProcess());
            processList.Add(new MutexProcess());
            processList.Add(new ReaderWriterLockSlimProcess2());
            
            for (int i = 0; i < ExecuteCount; ++i)
            {
                Console.WriteLine("Execute Count: " + (i + 1).ToString());
                for (int j = 0; j < processList.Count; ++j)
                {
                    processList[j].Execute(ThreadNum, TargetValue);
                }
            }

            //実行結果表示
            Console.WriteLine("");
            for (int j = 0; j < processList.Count; ++j)
            {
                processList[j].ResultAverage();
            }

            Console.WriteLine("Main 終了");
            Console.ReadKey();
        }
    }

    public abstract class ProcessBase
    {
        protected volatile int value = 0;
        private List<Tuple<int, long>> ExecutedInfoList = new List<Tuple<int, long>>();

        public void Execute(int processThreadNum, int targetValue)
        {
            Console.WriteLine("Start.    ThreadNo:" + Thread.CurrentThread.ManagedThreadId + " " + GetName());
            value = 0;
            var taskList = new List<Task>();
            var loopCount = targetValue / processThreadNum;
            var stopWatch = Stopwatch.StartNew();

            //引数指定の数だけスレッドを立ち上げる
            for (int i = 0; i < processThreadNum; ++i)
            {
                var task = Task.Run(() => { AddProcess(loopCount); });
                taskList.Add(task);
            }

            //全スレッドの終了を待機する
            //Task.Wait()はデッドロックの可能性があるので気を付ける事。
            //今回はコンソールアプリケーションなのでWait()でも問題ないので使用しています。
            Task.WhenAll(taskList).Wait();

            stopWatch.Stop();

            //Valueを表示
            ExecutedInfoList.Add(new Tuple<int, long>(value, stopWatch.ElapsedMilliseconds));
            Console.WriteLine("End. value:" + value + " Time(MS):" + stopWatch.ElapsedMilliseconds + "    ThreadNo:" + Thread.CurrentThread.ManagedThreadId + " " + GetName());
        }


        /// <summary>
        /// 派生先で異なる実装。複数スレッドでの処理
        /// </summary>
        public abstract void AddProcess(int loopCount);
        public abstract string GetName();

        /// <summary>
        /// 複数回実行したときの結果を出す
        /// </summary>
        public void ResultAverage()
        {
            long valueAve = 0;
            long timeAve = 0;
            for (int i = 0; i < ExecutedInfoList.Count; ++i)
            {
                valueAve += ExecutedInfoList[i].Item1;
                timeAve += ExecutedInfoList[i].Item2;
            }
            if (ExecutedInfoList.Count > 0)
            {
                valueAve = valueAve / ExecutedInfoList.Count;
                timeAve = timeAve / ExecutedInfoList.Count;
            }
            Console.WriteLine("Average value: " + valueAve + " Time(MS): " + timeAve + "    " + GetName());
        }
    }

    /// <summary>
    /// ロック制御をしない(値がおかしくなる実装)
    /// </summary>
    public class NoLockProcess : ProcessBase
    {
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                value++;
            }
        }
        public override string GetName()
        {
            return "NoLock";
        }
    }

    /// <summary>
    /// InterLockedクラスを使用して排他制御する
    /// </summary>
    public class InterLockedProcess : ProcessBase
    {
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                Interlocked.Increment(ref value);
            }
        }
        public override string GetName()
        {
            return "InterLocked";
        }
    }

    /// <summary>
    /// lock()構文を使用して排他制御する
    /// </summary>
    public class LockProcess : ProcessBase
    {
        private object lockObj = new object();
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                lock (lockObj)
                {
                    value++;
                }
            }
        }
        public override string GetName()
        {
            return "Lock";
        }
    }

    /// <summary>
    /// ReaderWriterLockSlimを使用して排他制御する。
    /// ReadLock同士は重複して実行可能。読み込み処理だけに使用できる
    /// UpgradeableReadLock同士は重複できないが、ReadLockは重複可能。WriteLockに昇格可能。書き換え前の読み込み処理に使用できる。
    /// WriteLockは全てのロックモードで重複不可。書き換え発生時に使用する。
    /// </summary>
    public class ReaderWriterLockSlimProcess : ProcessBase
    {
        private ReaderWriterLockSlim lockObj = new ReaderWriterLockSlim();
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                lockObj.EnterWriteLock();
                try
                {
                    value++;
                }
                finally
                {
                    lockObj.ExitWriteLock();
                }
            }
        }
        public override string GetName()
        {
            return "ReaderWriterLockSlim";
        }
    }
    /// <summary>
    /// ReaderWriteLockSlimをusing句で簡単に利用できるようパッキングを施した物
    /// </summary>
    public class ReaderWriterLockSlimProcess2 : ProcessBase
    {
        private ReaderWriterLockSlim lockObj = new ReaderWriterLockSlim();
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                using(var wl = new WriteLock(lockObj))
                {
                    value++;
                }
            }
        }
        public override string GetName()
        {
            return "ReaderWriterLockSlim PackVer";
        }
    }

    /// <summary>
    /// SemaphoreSlimを使用して排他制御する
    /// ロック中にasync/awaitが使用できる。
    /// つまりロック開始スレッドとロック終了スレッドが別でも良いのが特徴
    /// </summary>
    public class SemaphoreSlimProcess : ProcessBase
    {
        private SemaphoreSlim lockObj = new SemaphoreSlim(1, 1);
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                lockObj.Wait();
                try
                {
                    value++;
                }
                finally
                {
                    lockObj.Release();
                }

            }
        }
        public override string GetName()
        {
            return "SemaphoreSlimLock";
        }
    }

    public class MutexProcess : ProcessBase
    {
        private Mutex lockObj = new Mutex();
        public override void AddProcess(int loopCount)
        {
            for (int i = 0; i < loopCount; ++i)
            {
                lockObj.WaitOne();
                try
                {
                    value++;
                }
                finally
                {
                    lockObj.ReleaseMutex();
                }

            }
        }
        public override string GetName()
        {
            return "Mutex";
        }
    }
}

ReaderWriterLockのusing句便利パックはこちら

using System;
using System.Threading;

namespace LockTest
{
    // 構造体の理由はメモリ消費を抑えるため。
    // デフォルトコンストラクタを選択できてしまうのが嫌な場合はクラスに変更する

    /// <summary>
    /// ReaderWriterLockSlimのReadLockをUsing句で楽に使えるようにしたもの
    /// </summary>
    public struct ReadLock : IDisposable
    {
        ReaderWriterLockSlim obj;
        public ReadLock(ReaderWriterLockSlim lockObj)
        {
            obj = lockObj;
            lockObj.EnterReadLock();
        }
        public void Dispose()
        {
            obj.ExitReadLock();
        }
    }
    /// <summary>
    /// ReaderWriterLockSlimのUpgradeableReadLockをUsing句で楽に使えるようにしたもの
    /// </summary>
    public struct UpgradeableReadLock : IDisposable
    {
        ReaderWriterLockSlim obj;
        public UpgradeableReadLock(ReaderWriterLockSlim lockObj)
        {
            obj = lockObj;
            lockObj.EnterUpgradeableReadLock();
        }
        public void Dispose()
        {
            obj.ExitUpgradeableReadLock();
        }
    }
    /// <summary>
    /// ReaderWriterLockSlimのUpgradeableReadLockをUsing句で楽に使えるようにしたもの
    /// </summary>
    public struct WriteLock : IDisposable
    {
        ReaderWriterLockSlim obj;
        public WriteLock(ReaderWriterLockSlim lockObj)
        {
            obj = lockObj;
            lockObj.EnterWriteLock();
        }
        public void Dispose()
        {
            obj.ExitWriteLock();
        }
    }
}

バージョン管理ツール TortoiseGit導入と使い方

プログラミングをするならバージョン管理ツールは必ず導入したい所。

今の時代はGitを扱えた方が日々の仕事や就活で役に立つ機会が多いと思います。

今回はTortoiseGitを使ってGUI操作が可能なGit環境を構築します。

 

Gitを初めて扱う方にもわかりやすいように、イラストを交えて導入手順と普段の使い方を説明します。

 

1. Gitインストール

まず下からGitをダウンロードします。

赤く囲んだところをクリックすると最新のバージョンがダウンロード出来ます。

f:id:SionGames:20190209113820p:plain

https://git-scm.com

落としてきたインストーラーを起動してインストールして下さい。

インストーラーを起動すると色々と選択画面が出てきますが、特に修正なくNextで問題ないと思います。

 

2. TortoiseGitインストール

TortoiseGitをダウンロードします。

こちらもバージョンは最新のもので構いません。

OSのシステムに合わせて32bit/64bitを選択して下さい。

f:id:SionGames:20190209124715p:plain

https://tortoisegit.org/

赤丸で囲んだDownloadをクリックすると下のページが表示されます。

f:id:SionGames:20190209124802p:plain

 

LanguagePackを入れておくとGUIインターフェースが

日本語化されるのでわかりやすくなると思います。

インストール順は Git→TortoiseGit→LanguagePack でないといけないので注意して下さい。

f:id:SionGames:20190209124815p:plain

無事にインストールが終わった場合、適当なフォルダを右クリックするとTortoiseGitの項目が増えていると思います。

 適当なフォルダを右クリックする事でGit関連の操作が可能になっているはずです。

下の写真のように英語で表記されている場合、日本語化設定を行う必要があります。

f:id:SionGames:20190209205146p:plain

Settingsをクリックするとさらに画面が出てきます。

f:id:SionGames:20190209205305p:plain

Settings画面左側のGeneralをクリック後、Languageの項目で日本語を選択しましょう。

ここで日本語の項目が出ない場合、LanguagePackを再インストールして下さい。

3. Gitの簡単なデータイメージ

 操作説明に入る前にGitのデータの場所について図解しようと思います。

データの移動がイメージできるとGitを深く理解できるようになるはずです。

Gitのデータ構造は大きく3つに別れています。

f:id:SionGames:20190209225157p:plain

・ワーキングディレクト

 →Windows作業フォルダ上にある直接触れる所

・ローカルリポジトリ

 →後述の"コミット"でGitの管理下に置かれたファイル類。

・リモートリポジトリ

  →チームで一つ用意する。

  別のサーバーマシン上で保存されることが多い。

  GitHub等の外部サービスはこれを用意してくれている。

4. TortoiseGitで最初に行う操作

最初に作業フォルダとGitを紐づける処理を行う必要があります。

既にリモートリポジトリがGitHub等で存在する場合は、フォルダ右クリック→Gitクローンでリモートリポジトリの内容をマシン上に落してきて下さい。

f:id:SionGames:20190209231032p:plain

上の写真だとD:\work\test\ProjectD\AAAAAAフォルダ内に色々とコミットされたファイルが格納されるようになります。

 

リモートリポジトリが用意されておらず、ひとまずバージョン管理のみ行いたい場合はフォルダ右クリック→ここにリポジトリを作成を選択して下さい。

f:id:SionGames:20190209232533p:plain

Bareを生成にチェックを入れないようにする事で

作業ディレクトリとローカルリポジトリのみの構成に出来ます。 

通常はリモートリポジトリを別のマシンに置く事で、バックアップ的側面を持たせることが出来ます。できればリモートリポジトリを別のマシンに用意する事をお勧めします。

また、リモートリポジトリが無い場合は後述する"プル"や"プッシュ"が出来なくなります。

5. TortoiseGitで普段行う操作

基本的には 作業前にプル→ファイル修正→コミット→プル→プッシュ

 

"プル"

多人数で一つのリモートリポジトリを使用している場合は変更作業前に一度"プル"を行ってから作業するように癖を付けましょう。これを行うことで無用な"競合"の発生を抑えることが出来ます。

"プル"を行うことで他人がリモートリポジトリに対して適用した変更を、自分の作業フォルダまで落としてくることが出来ます。

f:id:SionGames:20190210001537p:plain

"プル"の具体的な操作はフォルダを右クリック→TortoiseGit→プルです。

f:id:SionGames:20190210003922p:plain

出てきたウィンドウでOKボタンを押すとプルが完了します。

"コミット"

作業を進めて区切りが良くなったら一度コミットを行いましょう。

f:id:SionGames:20190210010845p:plain

f:id:SionGames:20190210011344p:plain

f:id:SionGames:20190210011707p:plain

上の写真では既存の説明書.txtの内容を修正して、新しい追加仕様.txtを新しくGit管理下に追加するという内容です。 

新規ファイルが表示されない場合、バージョン管理外ファイルのファイルを表示にチェックが入っているか確認して下さい。

コミットボタンを押すことでローカルリポジトリに対して変更を適用できます。

"プッシュ"

"コミット"した内容をリモートリポジトリに適用します。

f:id:SionGames:20190210032711p:plain

"プル"の具体的な操作はフォルダを右クリック→TortoiseGit→プッシュです。

f:id:SionGames:20190210033319p:plain

出てきたウィンドウで特に触らずOKボタンを押すと完了です。

6. 競合対処

複数人で作業しているとよく競合を起こします。

同じファイル、同一行を修正コミットした場合に発生します。

f:id:SionGames:20190210040343p:plain

 競合の解決を押すとウィンドウが出てきます。

MERGE_HEAD(origin/ブランチ名)を使って競合を解決を押すと、自分の修正を無かったことにして競合を解決します。

HEADを使って競合を解決を押すと、自分の修正が正しい物として押し通します。

今回は手入力で競合を修正してみます。

f:id:SionGames:20190210040709p:plain

下の写真だと自分が超ダッシュに書き換えてコミットしている間に、他の人が誤字を修正してコミットプッシュしたので競合を起こした例です。

f:id:SionGames:20190210040939p:plain

実際はDashが正しかった。

4行目に空行がでてきそうに見えますがこれが正しい修正です。よく見ると改行マークが無いのが確認できます。

この内容で競合を解決してみましょう。

f:id:SionGames:20190210041223p:plain

 全ての競合を解決した後にコミットを行って下さい。

すると下の画面が出てきます。

f:id:SionGames:20190210040927p:plain

注意しなければならないのは、写真に書かれている通り修正した覚えのないファイルが表示される事があることです。

競合のマージコミットでは他人のコミット内容がここに表示されることがよくあります。

不安に思ってチェックを外してしまいがちですが、そうすると巻き戻りという恐ろしい現象が発生しますので外さないでください。

慣れないうちは他の人に確認を取るなどした方が良いかもしれません。

f:id:SionGames:20190210042001p:plain

上のような画面が表示されるかもしれませんが、これはメッセージの内容がデフォルトのままで良いのかという確認ウィンドウです。

大抵の場合デフォルトのままで良いので、このメッセージを表示しないにチェックを入れた後、無視を押してコミットを完了させましょう。

コミット後はプッシュして下さい。

7. あとがき

今回はブランチ一本で作業している環境の前提で説明しました。

複数ブランチを使用するプロジェクトだと、ブランチ作成やブランチ切り替え、フェッチ、マージを覚える必要があります。

Gitに触れたことが無い人もこの記事を足掛かりに少しずつGitを習得していきましょう!