こちらは 1. 2. のチュートリアルが完了していることが前提の内容となっております。
はじめに
続いて Diarkis のカスタマイズ性を体験するために、カスタムコマンドを実装してみましょう。
本チュートリアルでは以下のようなコマンドを実装します。
コマンドバージョン: 2 (0, 1 はビルトインコマンドで利用されています)
リクエスト: 攻撃タイプ (type) を指定してコマンドを送信 (1: 近接、2:遠距離)
レスポンス: 誰がダメージを与えたか、ダメージ値と合計ダメージ値を返却
実装の流れは以下を想定しています。
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 型
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