[ゲーム]ソケット通信でキー入力同期を行う


#この記事では
スタンドアローンな自作ゲームに通信機能を追加して勉強しています。操作キャラのモーションと座標の同期が完成したのでその方法を投稿したいと思います。

リポジトリはこちらです。
https://github.com/MIZU-DON/MicuAca/tree/master/Goal3(Qiita%E6%8A%95%E7%A8%BF%E5%88%86)

ゲームは3Dアクションゲームです。複数人のプレイヤーがフィールド上でモーションと座標を同期し一緒に戦えるようにしていきます。
![ダウンロード.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403087/25ca108b-4358-012f-462e-b1d8f7f480eb.gif)




#サーバー
並列処理を簡単にできそうという理由からGo言語を使用しています。

参考URL(https://qiita.com/ak-ymst/items/259960bfffa42d51d246)

```golang

package main

import (
	"fmt"
	"io"
	"net"
	"strconv"
)

var connList []net.Conn

func main() {
	service := ":10000"
	listener, error := net.Listen("tcp", service)

	if error != nil {
		panic(error)
	}

	fmt.Printf("Server running at %s)\n", service)
	fmt.Println("サーバ受付待機中…")

	waitClient(listener)
}

func waitClient(listener net.Listener) {
	connection, error := listener.Accept()

	connList = append(connList, connection)

	fmt.Println("通信スタート！")
	fmt.Printf("接続数 : %v \n", len(connList))
	fmt.Println("ローカルアドレス :", connList[len(connList)-1].LocalAddr())
	fmt.Println("リモートアドレス :", connList[len(connList)-1].RemoteAddr())
	fmt.Println()

	// 接続先に、サーバーに何番目に接続したかを伝える(クライアントを親と子で分ける)
	var buf = make([]byte, 1024)
	var connCnt = len(connList) - 1 // 接続数を送信 0スタートにしたいから-1
	var str string
	str = strconv.Itoa(connCnt)
	buf = []byte(str)
	_, error = connection.Write(buf[:1])

	if error != nil {
		panic(error)
	}

	go goEcho(connection)

	waitClient(listener)
}

func goEcho(connection net.Conn) {
	defer connection.Close()
	echo(connection)
}

func echo(connection net.Conn) {

	var buf = make([]byte, 1024)

	// 受信
	n, error := connection.Read(buf)
	if error != nil {
		if error == io.EOF {
			return
		} else {
			panic(error)
		}
	}

	fmt.Printf("受信(%s) : %s \n",
		connection.RemoteAddr(), buf[:n]) // リモートアドレスと受け取った内容を表示

	// 送信処理
	for i := range connList {
		fmt.Printf("***送信元と送信先の宛先が同じ場合は送信しない***\n")
		fmt.Printf("受信先(%s)\n", connList[i].RemoteAddr())
		fmt.Printf("送信元(%s)\n", connection.RemoteAddr())

		// 送信元には返さないようにする
		if(connList[i] == connection){
			fmt.Printf("送信元と送信先の宛先が同じなのでcontinue")
			continue
			} 

		n, error = connList[i].Write(buf[:n])
		if error != nil {
			panic(error)
		}
		fmt.Printf("送信(%s) : %s \n",
			connList[i].RemoteAddr(), buf[:n])
	}
	fmt.Println()

	echo(connection)
}

```
#解説
サーバーは受信した文字列を受信元以外の通信相手全員に送信するようにしています。
補足：(A,B,C,D が通信しているとしたらAから受信したデータをB,C,Dに送信します。Bから受信したデータはA,C,Dに送信...というイメージです。)

#クライアント
#座標の同期方法
json形式で入力情報を送受信して同期する様にしました。

```c++:Game.cpp

	void Game::Update() {
	m_Ms.UpdatePointLight();

	/*状態管理*/
	Input();
	m_GameState->Update(this);

	m_EffectMgr->Update();
	m_FragmentMgr->Update();
	m_BltMgr->Update();

	/*キャラクリア*/
	std::vector<SPTR<Character>>::iterator it = m_CharaList.begin();
	while (it != m_CharaList.end()) {
		if (!(*it)->GetVisibleFlg() && (*it) != m_Player) {
			it = m_CharaList.erase(it);
		}
		else {
			it++;
		}
	} //end while

	/*=====送受信=====*/
	// jsonデータにして座標を送る
	string jsonStr; // jsonファイルの文字列
	json11::Json json; // jsonデータ

	// 送信
	m_Player->UpdateJson();
	json = m_Player->GetJson();
	jsonStr = json.dump();
	m_TcpClient->Send<string>(jsonStr);

	// 受信
	jsonStr = m_TcpClient->RecvAndGetString();
	json = Helper::JsonStrToPureJson(jsonStr);
	m_OnlinePlayer->SetJson(json);
	/*================*/

	// カリングと描画順を決める
	m_RenderMgr->Update();
}
```
#解説
ゲームの更新を大方終わらした後に、自分の入力情報を送信する事と通信相手の入力情報を受信しています。


#サーバーから受け取った情報を元にキャラクターを動作させる
```c++:OnlineCharacterInputComponent.cpp
void My::OnlineCharacterInputComponent::Update()
{
	// 入力情報をjsonで更新
	auto&& json = m_Owner->GetJson();
	if (json.dump() != "null") { // 何も入っていないときの文字列は"null"
		auto&& inputData = json["inputData"].array_items()[0];
		m_MouseLButton = inputData["mouseLButton"].bool_value();
		m_MouseRButton = inputData["mouseRButton"].bool_value();
		m_Key_W = inputData["key_W"].bool_value();
		m_Key_A = inputData["key_A"].bool_value();
		m_Key_S = inputData["key_S"].bool_value();
		m_Key_D = inputData["key_D"].bool_value();
		m_HistoryMouseLButton = inputData["HoldMouseLButton"].bool_value();
	}
}
```

```c++:PlayerState.cpp
SPTR<PlayerState> IdleState::HandleInput(const Player& player){
	if (player.GetDamageCnt() > 0.0f) {
		return MAKE_S<DamageState>();
	}

	auto&& input = player.GetComponent<InputComponent>();
	if (input->GetKey_W() ||
		input->GetKey_A() ||
		input->GetKey_S() ||
		input->GetKey_D())
	{
		return MAKE_S<DushState>();
	}

	if (input->GetMouseLButton() &&
		!input->IsHoldMouseLButton()) {
		return MAKE_S<AttackState>();
	}

	if (input->GetMouseRButton()) {
		return MAKE_S<ChantState>();
	}

	return nullptr;
}
```
#解説
サーバーから受け取った入力情報をinputDataに格納します。inputData通りに動作するキャラクターを用意してアクションと座標の同期を実装しました。

#最後に
初めて通信機能を作成したので改善点は多いと思います。自分のようにゲーム通信に初めて手を出す方の参考になれば幸いです。