3. カスタムコマンドを実装する

こちらは 1. 2. のチュートリアルが完了していることが前提の内容となっております。

はじめに

続いて Diarkis のカスタマイズ性を体験するために、カスタムコマンドを実装してみましょう。

本チュートリアルでは以下のようなコマンドを実装します。

  • Room の敵に攻撃するコマンド

  • コマンドバージョン: 2 (0, 1 はビルトインコマンドで利用されています)

  • コマンド ID: 300

  • リクエスト: 攻撃タイプ (type) を指定してコマンドを送信 (1: 近接、2:遠距離)

  • レスポンス: 誰がダメージを与えたか、ダメージ値と合計ダメージ値を返却

  • ルームメンバー全員に push 通知

実装の流れは以下を想定しています。

  • puffer (Diarkis が提供するデータシリアライザ) を使ってペイロードを生成するためのコードを出

  • サーバーにカスタムコマンドを実装

  • テストクライアントにコマンドを実装

Puffer モジュール(Diarkis が提供する データシリアライザ)

Puffer は JSON からデータをシリアライズ、デシリアライズするためのコードを生成するツールです。

出力先は C++、C#、Go に対応しているため、Unreal や Unity で開発するクライアントと、Go で開発するサーバーとのパケットデータに関する実装を簡略化できます。

Puffer の詳細については API リファレンスをご確認ください。

https://docs.diarkis.io/docs/server/v1.0.0/diarkis/puffer/index.html

カスタムコマンドのデータ定義用 JSON ファイルの作成

💾 ./puffer/json_definitions/custom/attack.json を追加します。

attack はカスタムコマンドのリクエストに利用するデータです。

  • type: 攻撃タイプを uint8 型で格納できます

attackResult はレスポンス、ルームメンバーへの push 通知に使うデータです。

  • type: 攻撃タイプを uint8 型で格納できます

  • uid: 誰がダメージを与えたか。 string 型

  • damage: ダメージ値。 uint16 型

  • totalDamage: 合計ダメージ値。 uint16 型

{
  "attack": {
    "ver": 2,
    "cmd": 300,
    "package": "custom",
    "properties": {
      "type": "u8"
    }
  },
  "attackResult": {
    "ver": 2,
    "cmd": 300,
    "package": "custom",
    "properties": {
      "type": "u8",
      "uid": "string",
      "damage": "u16",
      "totalDamage": "u16"
    }
  }
}

コード生成

以下のコマンドを実行して、コードを生成します。puffer 以下の各言語のディレクトリにコードが出力されます。

$ make gen
# puffer 以下の各言語のディレクトリにコードが出力されます
puffer
├── cpp/custom/Attack.h
├── cpp/custom/AttackResult.h
├── cs/custom/Attack.cs
├── cs/custom/AttackResult.cs
├── go/custom/attack.go
└── go/custom/attackresult.go

サーバーに attack コマンドハンドラを実装する

サーバーに attack コマンドを実装しましょう。

💾 ./cmds/custom/attack.go を追加します。

コード内のコメントにて、どんなことを実装しているか記載していますので、合わせてお読みください。

package customcmds

import (
	"errors"
	// pattack は先程生成した puffer のパッケージです。
	pattack "handson/puffer/go/custom"

	"github.com/Diarkis/diarkis/derror"
	"github.com/Diarkis/diarkis/room"
	"github.com/Diarkis/diarkis/server"
	"github.com/Diarkis/diarkis/user"
	"github.com/Diarkis/diarkis/util"
)

// attack は Room 内の敵に対して攻撃します。
// 
// Diarkis の全てのコマンドは決まった引数を取ります。
// - ver: コマンドのバージョン
// - cmd: コマンド ID
// - payload: コマンドにわたすデータ
// - userData: ユーザーデータ
func attack(ver uint8, cmd uint16, payload []byte, userData *user.User, next func(error)) {

	// attack コマンドは Room の敵に攻撃するコマンドなので、
	// Room に参加していない場合はエラーとなります。
	// Room に参加しているかどうかは roomID を取得して確認します。
	roomID := room.GetRoomID(userData)
	// room に参加していない場合は、 roomID は空で返却されるので、エラーハンドリングを行います
	if roomID == "" {
		// ユーザーに userData.ServerRespond() でエラーのレスポンスを返します。
		err := errors.New("not in the room")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.NotAllowed(0)), ver, cmd, server.Bad, true)
		// レスポンスを返したら next(err) して return します。
		next(err)
		return
	}

	// pattack.NewAttack() したあとに、req.Unpack(payload) することで
	// ペイロードをデシリアライズできます。
	req := pattack.NewAttack()
	req.Unpack(payload)

	// Type を元にダメージを計算します。
	damage := 0
	switch req.Type {
	case 1: // 近接攻撃 (D20)
		damage = util.RandomInt(1, 20)
	case 2: // 遠距離攻撃 (D12 + 3)
		damage = util.RandomInt(1, 12) + 3
	default:
		err := errors.New("invalid attack type")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.InvalidParameter(0)), ver, cmd, server.Bad, true)
		next(err)
		return
	}

	// 算出したダメージを敵の合計ダメージに加算します。
	// Room に Property として情報を保存することができます。
	// ここでは "DAMAGE" というキーに対してダメージを加算しています。
	// room.IncrProperty を使うと加算後の数値を取得することができます。
	// その他にも Property を扱うための関数が用意されています。詳細は以下をご覧ください。
	// https://docs.diarkis.io/docs/server/v1.0.0/diarkis/room/index.html
	updatedDamage, updated := room.IncrProperty(roomID, "DAMAGE", int64(damage))
	if !updated {
		err := errors.New("incr property failed")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.Internal(0)), ver, cmd, server.Err, true)
		next(err)
		return
	}
	logger.Info("Room %s has been attacked by %s using %s attack by %d damage. Total damage: %d", roomID, userData.ID, req.Type, damage, updatedDamage)

	// 誰がダメージを与えたか、ダメージ値と合計ダメージ値を返却し、ルームメンバーに通知します。
	// res := pattack.NewAttackResult() したあとに返却するデータをセットします。
	res := pattack.NewAttackResult()
	res.Type = req.Type
	res.Uid = userData.ID
	res.Damage = uint16(damage)
	res.TotalDamage = uint16(updatedDamage)

	// コマンドを実行したユーザーには userData.ServerRespond()
	// 他のルームメンバーには room.Relay() を使って結果を通知します。
	userData.ServerRespond(res.Pack(), ver, cmd, server.Ok, true)
	room.Relay(roomID, userData, ver, cmd, res.Pack(), true)
	// レスポンスを返したら next(nil) して return します。
	next(nil)
}

attack コマンドを公開する

コマンドを実装したら、外部に公開する必要があります。

💾 ./cmds/custom/main.go を編集し、以下を追加します。

Expose() に対して、 diarkisexec.SetServerCommandHandler() を追加して、attack コマンドを公開します。

diff --git a/cmds/custom/main.go b/cmds/custom/main.go
index f2374ed..e98f5ef 100644
--- a/cmds/custom/main.go
+++ b/cmds/custom/main.go
@@ -37,6 +37,7 @@ func Expose() {
        diarkisexec.SetServerCommandHandler(custom.GetFieldInfoVer, custom.GetFieldInfoCmd, getFieldInfo)
        diarkisexec.SetServerCommandHandler(CustomVer, getUserStatusListCmdID, getUserStatusList)
        diarkisexec.SetServerCommandHandler(CustomVer, resonanceCmdID, resonanceCmd)
+       diarkisexec.SetServerCommandHandler(custom.AttackVer, custom.AttackCmd, attack)
 }

テストクライアントに attack コマンドを追加する

サーバーの attack コマンドを実行するためにテストクライアントに attack コマンドを追加します。

💾 ./testcli/handson/attack.go を追加します。

クライアントを実行するための setup の他、サーバーにコマンドを送信する Attack() 、レスポンスをハンドリングする onResponse()、push通知をハンドリングする onPush() を実装しています。

package handson

import (
	"fmt"

	pattack "handson/puffer/go/custom"

	"github.com/Diarkis/diarkis/client/go/tcp"
	"github.com/Diarkis/diarkis/client/go/udp"
)

type Handson struct {
	tcp *tcp.Client
	udp *udp.Client
}

func SetupHandsonAsTCP(c *tcp.Client) *Handson {
	h := &Handson{tcp: c}
	h.setup()
	return h
}

func SetupHandsonAsUDP(c *udp.Client) *Handson {
	h := &Handson{udp: c}
	h.setup()
	return h
}

func (h *Handson) setup() {
	if h.tcp != nil {
		h.tcp.OnResponse(h.onResponse)
		h.tcp.OnPush(h.onPush)
		return
	}
	if h.udp != nil {
		h.udp.OnResponse(h.onResponse)
		h.udp.OnPush(h.onPush)
	}
}

func (h *Handson) onResponse(ver uint8, cmd uint16, status uint8, payload []byte) {
	if ver != pattack.AttackVer || cmd != pattack.AttackCmd {
		return
	}
	if status != uint8(1) {
		fmt.Printf("Attack failed: %v\n", string(payload))
		return
	}
	res := pattack.NewAttackResult()
	err := res.Unpack(payload)
	if err != nil {
		fmt.Printf("Failed to unpack attack response: %v\n", err)
		return
	}
	fmt.Printf("You dealt %d damage. Total damage: %d\n", res.Damage, res.TotalDamage)
}

func (h *Handson) onPush(ver uint8, cmd uint16, payload []byte) {
	// no push
	if ver != pattack.AttackVer || cmd != pattack.AttackCmd {
		return
	}
	res := pattack.NewAttackResult()
	err := res.Unpack(payload)
	if err != nil {
		fmt.Printf("Failed to unpack attack response: %v\n", err)
	}
	fmt.Printf("%s dealt %d damage. Total damage: %d\n", res.Uid, res.Damage, res.TotalDamage)
}

func (h *Handson) Attack(attackType uint8) {
	req := pattack.NewAttack()
	req.Type = attackType
	if h.tcp != nil {
		h.tcp.Send(pattack.AttackVer, pattack.AttackCmd, req.Pack())
		return
	}
	if h.udp != nil {
		h.udp.Send(pattack.AttackVer, pattack.AttackCmd, req.Pack())
	}
}

テストクライアントに handson attack コマンドを登録します。

💾 ./testcli/main.go を編集します。

cli.RegisterCommands(string, []cli.Command) でテストクライアントに新しいコマンドを登録できます。

diff --git a/testcli/main.go b/testcli/main.go
index bdca4ab..2dbdd2c 100644
--- a/testcli/main.go
+++ b/testcli/main.go
@@ -3,8 +3,11 @@ package main
 import (
        "bufio"
        "fmt"
+       "handson/testcli/handson"
        "handson/testcli/resonance"
        "os"
+       "strconv"
+       "strings"

        "github.com/Diarkis/diarkis/client/go/test/cli"
 )
@@ -12,17 +15,24 @@ import (
 var (
        tcpResonance *resonance.Resonance
        udpResonance *resonance.Resonance
+       tcpHandson   *handson.Handson
+       udpHandson   *handson.Handson
 )

 func main() {
        cli.SetupBuiltInCommands()
        cli.RegisterCommands("test", []cli.Command{{CmdName: "resonate", Desc: "Resonate your message", CmdFunc: resonate}})
+       cli.RegisterCommands("handson", []cli.Command{
+               {CmdName: "attack", Desc: "Attack for hands-on", CmdFunc: attack},
+       })
        cli.Connect()
        if cli.TCPClient != nil {
                tcpResonance = resonance.SetupAsTCP(cli.TCPClient)
+               tcpHandson = handson.SetupHandsonAsTCP(cli.TCPClient)
        }
        if cli.UDPClient != nil {
                udpResonance = resonance.SetupAsUDP(cli.UDPClient)
+               udpHandson = handson.SetupHandsonAsUDP(cli.UDPClient)
        }
        cli.Run()
 }
@@ -45,3 +55,30 @@ func resonate() {
                udpResonance.Resonate(message)
        }
 }
+
+func attack() {
+       reader := bufio.NewReader(os.Stdin)
+       fmt.Println("Which client to join a room? [tcp/udp]")
+       client, _ := reader.ReadString('\n')
+       fmt.Println("Enter the attack type. [1: melee, 2: range]")
+       attackTypeStr, _ := reader.ReadString('\n')
+       attackTypeStr = strings.Trim(attackTypeStr, "\n")
+       attackType, err := strconv.Atoi(attackTypeStr)
+       if err != nil || attackType != 1 && attackType != 2 {
+               fmt.Println("Invalid attack type.")
+               return
+       }
+
+       switch client {
+       case "tcp\n":
+               if tcpHandson == nil {
+                       return
+               }
+               tcpHandson.Attack(uint8(attackType))
+       case "udp\n":
+               if udpHandson == nil {
+                       return
+               }
+               udpHandson.Attack(uint8(attackType))
+       }
+}

バイナリを生成し、udp サーバーを再起動する

再度 make build-local を実行してバイナリを生成します。

サーバーとテストクライアントが更新されます。

エラーが無いことが確認できたら、一度 udp サーバーを停止して、再起動してください。

attack コマンドの動作確認

room create room join した後に handson attack コマンドを発行し、ダメージが加算されることを確認してください。

uid: test1 で handson attack を実行

> handson attack
Which client to join a room? [tcp/udp]
udp
Enter the attack type. [1: melee, 2: range]
1
You dealt 13 damage. Total damage: 13

uid: test2 で attack 結果の push 通知を受信

[UID: test2][SID(UDP): zzzz][RoomID: yyyy]
 > test1 dealt 13 damage. Total damage: 13

最終更新