10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unity+Node.jsで1対1の簡単なオンラインシューティングゲームを作った話 導入編

Last updated at Posted at 2019-05-12

この記事に関わる記事一覧

導入編(現在の記事)
Node.js編
Unity編

はじめに

Node.jsを使ってみたく、実際に手を動かして何か作ってみるのが一番だと思い、
自分なりにプレイヤーのマッチングや同期の取り方を考えてやってみようと思いました。

Node.js(サーバー側)よりもUnity(クライアント側)で苦戦しました...

私はNode.js,リアルタイム通信の知識がそこまであるわけではないので、
素人なりにどう考えて実装していったかの記録を残していきたいと思い記事を書き始めました。

コードの書き方に正解はないと思うので、これから書いていく記事を通して
今回作成したゲームの作り方の概念だけ書いていこうかなと思います。

プロジェクト

ソースコード公開しました!
クライアントプロジェクト(Unity)
サーバープロジェクト(Node.js)

実行方法

1.対戦するのには2つクライアントが必要なので、あらかじめビルドしておきます
2.サーバープロジェクトのindex.jsをnodeで実行
3.クライアントプロジェクトのMenuシーンと1でビルドしたアプリを実行
4.両アプリでマッチング開始

操作方法

ドラッグ:移動
タップ :自機が向いている方向に弾を発射

スクリーンショット

IMG_5512.PNG IMG_5514.PNG

通信プロトコル

今回はUDPを選択しました。
マルチ対戦で使える通信プロトコルは以下の3種類あると思います。

通信プロトコル 特徴
UDP 速い!!でも、ちゃんと届いたか、届く順番が保証されない
TCP UDPとは違い確実に相手に届ける。(UDPよりは遅い)
WebSocket 低コストのTCP?Webで使うソケット通信

WebSocketはよく分かっていないのですが、
Webブラウザで手軽に双方向通信ができる!!そんなイメージが私の中にあります。
詳しいことは調べていただければと思います...

今回作成するのは毎フレーム同期が必要なリアルタイムで動くシューティングゲームなのでUDPにしました。

同期の取り方

はじめの方は入力情報だけ同期すれば大丈夫だろうと考えていました。
しかし、実装していくうちに2つのクライアント間で状態が異なっていき
終いにはプレイヤーの位置が違う、勝敗結果が異なるということになってしまいました。

今回は自前で衝突応答、判定などを作るのが面倒くさかったので、Rigidbody2D使ってしまおうと
考えたのがよくありませんでした。

Unityの中身で物理の処理がどのタイミングで実行されているのかを把握するのが難しいので、
このくらいの簡単なゲームであれば、自分で衝突判定や衝突応答を書いてしまって実行されるタイミングを
管理できるようにした方が後ほど苦しまなくなると思います。

結果的には以下の3つで同期を取ることになりました。

  • 入力情報(バーチャルJoystic、発射)
  • 座標
  • 弾が当たったかどうかの判定

また今回は通信する情報をJson形式で扱いました。

位置と入力の同期

まず何も考えずにプレイヤーを動かしてと言われたら、
毎フレーム入力に応じてプレイヤーを動かす処理を書くと思います。

しかし、このように実装してしまっては同期は取れません。

プレイヤーAとBがいるとします。
プレイヤーAの端末では現在5フレーム目で、プレイヤーBの端末も現在5フレームであったとします。
しかし、プレイヤーAの端末からプレイヤーBの端末まで情報が届くのが同じ5フレーム内であるという保証はありません。
通信には遅延が付き物です。
また、端末によってフレームの更新速度も違うのでプレイヤーAの端末では5フレーム目でも、
プレイヤーBの端末は4フレーム目が実行中かもしれません。

自分の端末で自機を動かしていて、情報が到着次第相手機を動かすような実装では同期は取れません。
この状態で作っていって最後に同期が取れないと気づいた頃には後の祭りです。

詳しくはUnity編で書きたいと思うのでここではざっくりと書きます。

Queueというデータ構造を使って入力情報を毎フレーム入れていき、相手の入力情報が届き次第
Queueから自分の入力情報を取り出して相手の入力情報と一緒に実行します。

キューとは
オブジェクトをどんどん追加していけるデータ構造で
一番最初に追加した古いオブジェクトから順に取り出していきます。(先入れ先出し)

なので、入力した情報は数フレーム遅れて実行されることになります。
私は3フレーム以内に相手の情報が届かなかったら、自分の入力情報を追加することをやめて待つようにしました。
もし待たなければ、情報の到着が30フレーム遅れて到着した場合約0.5秒前の自分の操作が実行されてしまいます。

位置と入力の同期では以下のJSONで送り合いました。

{
	"type":"input",
	"own":"{\"id\":\"16aab79959a24f\",\"name\":\"Black\",\"port\":\"\",\"address\":\"\"}",
	"rival":"{\"id\":\"16aab79983f274\",\"name\":\"White\",\"port\":\"61374\",\"address\":\"***.***.**.**\"}",
	"requireNextFrame":12,
	"inputObjects":
	[
		"{\"frame\":13,\"axisX\":0.0,\"axisY\":0.0,\"isFire\":false,\"fireDirX\":0.0,\"fireDirY\":0.0,\"posX\":0.0,\"posY\":-5.0,\"rotZ\":0.0}",
		"{\"frame\":14,\"axisX\":0.0,\"axisY\":0.0,\"isFire\":false,\"fireDirX\":0.0,\"fireDirY\":0.0,\"posX\":0.0,\"posY\":-5.0,\"rotZ\":0.0}"
	]
}
Key 内容
type このJSONは何の情報かを判別するためのもの(全てのJSONに付けた)
own 自分の情報のJSON文字列(UserObj)
rival 相手の情報のJSON文字列(UserObj)
requireNextFrame 現在何フレーム目からの相手の入力情報が欲しいか
inputObjects 位置情報・入力情報(配列)(InputObj)

C#でJSONをパースする都合で、UserObj,InputObjをJSON文字列にしています。

UserObj

Key 内容
id ユーザーの識別ID(使わなかった)
name ユーザー名
port ポート番号
address グローバルIPアドレス

自分(own)のアドレス、ポート番号は特に必要がなかったので入れませんでした。

InputObj

Key 内容
frame 何フレーム目の情報か
axisX バーチャルパッドのX軸(-1.0~1.0)
axisY バーチャルパッドのY軸(-1.0~1.0)
isFire 弾が発射されたか(false,true)
fireDirX 弾の発射方向(X方向)
fireDirY 弾の発射方向(Y方向)
posX 現在の位置X
posY 現在の位置Y
rotZ 現在の回転角度Z(degree)

InputObjを複数送る理由は、UDP通信だとパケットロストが発生して情報が届かない時があるからです。
例えば5フレーム目で送った入力情報が相手に届かず、6フレームの入力情報は相手に届いた場合でも、
6フレームの入力情報に5フレーム目の入力情報が含まれているので届けることができます。

弾が当たったかどうかの同期

位置・入力の同期が取れたことでプレイヤー同士の位置がずれることなくなって満足していました...

しかし、何回かプレイをしてみると2つの端末の間でHPの値が異なったのです。
多分以下の原因があると思います。

  • 計算の誤差があり弾が当たっていたり、当たらなかったり2つの端末で結果が異なった
  • 弾の位置の同期はしていなかったので物理演算の結果が異なっていた

本来弾の位置の同期もとったほうがいいとは思ったのですが、面倒くさかったので
ある弾がプレイヤーに当たった場合、JSONで相手に情報を送って
お互いの端末でその弾がプレイヤーに当たっていた場合ダメージ判定をするようにしました。

弾の当たった情報を以下のJSON形式で相手に送りました。

{
	"type":"hit-bullet",
	"bulletType":"BLUE",
	"fireFrame":152,
	"own":"{\"id\":\"16aabc7087860\",\"name\":\"Black\",\"port\":\"\",\"address\":\"\"}",
	"rival":"{\"id\":\"16aabc70728244\",\"name\":\"White\",\"port\":\"62167\",\"address\":\"***.***.**.**\"}"
}
Key 内容
type このJSONは何の情報かを判別するためのもの(全てのJSONに付けた)
bulletType 弾の色(BLUE,RED)
fireFrame 当たった弾が発射されたフレーム
own 自分の情報のJSON文字列(UserObj)
rival 相手の情報のJSON文字列(UserObj)

成果物で動きを確認していただければ分かると思うのですが、
お互いの端末で自機は赤で、相手機は青で表示されています。
なので、送るときはbulletTypeの色を逆にして送りました。

1フレームに打てる弾は1個なのでfireFrameはその弾固有のIDになります。

自分の端末で当たった弾の情報をListに追加していき、
相手から送られてきた弾の情報もListに追加していきます。
この二つのListを照らし合わせたときにbulletType,fireFrameが一致するものがあれば、
bulletTypeに応じて相手または自分へのダメージ処理を行うようにしました。

最後に

次回はNode.jsの実装について書いていきたいと思います。
ほとんど自分の備忘録みたいな感じで書いているので分かりにくいところが多かったと思います。
今回の記事を書き終えてみて、もっと情報を整理してプレイヤーの位置だけを同期するサンプルを作成して
ワークショップ形式で説明を書いたほうが分かりやすいかなと思いました。
このシリーズの記事を書き終えたらそっちの方も書いてみようかなと思います。

自分なりの方法でやっているので、間違っているところがあるとは思いますがご了承ください。
最後まで記事を読んでいただきありがとうございました!

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?