代码示例 #

本页提供了一些可用功能的常见示例,在使用Go运行库开发项目时,可以将这些功能用作模板。

比赛处理程序 #

比赛处理程序代表所有服务器端函数,用于处理权威多人游戏比赛的游戏输入和操作。请参阅比赛处理程序 API比赛运行时 API参考页面,了解比赛处理程序功能。

这是乒乓比赛处理程序示例。服务器接收到的消息将被广播回发送消息的对等方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package example

import (
    "context"
    "database/sql"
    "strconv"

    "github.com/heroiclabs/nakama-common/runtime"
)

type MatchState struct {
    presences map[string]runtime.Presence
}

type Match struct{}

func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
    logger.Info("Hello Multiplayer!")
    err := initializer.RegisterMatch("standard_match", newMatch)

    if err != nil {
        logger.Error("[RegisterMatch] error: ", err.Error())
        return err
    }

    return nil
}

func newMatch(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (m runtime.Match, err error) {
    return &Match{}, nil
}

func (m *Match) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) {
    state := &MatchState{
        presences: make(map[string]runtime.Presence),
    }

    tickRate := 1
    label := ""

    return state, tickRate, label
}

func (m *Match) MatchJoinAttempt(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence, metadata map[string]string) (interface{}, bool, string) {
    acceptUser := true

    return state, acceptUser, ""
}

func (m *Match) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
    mState, _ := state.(*MatchState)

    for _, p := range presences {
        mState.presences[p.GetUserId()] = p
    }

    return mState
}

func (m *Match) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
    mState, _ := state.(*MatchState)

    for _, p := range presences {
        delete(mState.presences, p.GetUserId())
    }

    return mState
}

func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} {
    mState, _ := state.(*MatchState)

    for _, presence := range mState.presences {
        logger.Info("Presence %v named %v", presence.GetUserId(), presence.GetUsername())
    }

    for _, message := range messages {
        logger.Info("Received %v from %v", string(message.GetData()), message.GetUserId())
        reliable := true
        dispatcher.BroadcastMessage(1, message.GetData(), []runtime.Presence{message}, reliable)
    }

    return mState
}

func (m *Match) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} {
    message := "Server shutting down in " + strconv.Itoa(graceSeconds) + " seconds."
    reliable := true
    dispatcher.BroadcastMessage(2, []byte(message), []runtime.Presence{}, reliable)

    return state
}

func (m *Match) MatchSignal(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, data string) (interface{}, string) {
    return state, "signal received: " + data
}

上下文 #

此示例演示如何提取调用用户的 ID 和作为环境变量存储的密钥:

1
2
3
4
5
6
7
8
9
// Getting the calling user ID from the context
userId  := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)

// Getting environment variables from the context
env := ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string)
secretKey, ok := env["SECRET_KEY"]
if !ok {
  // Did not find the environment variable
}

数据库处理程序 #

此示例创建系统 ID(不能从客户端使用的 ID),自定义 SQL 查询将其插入 users 表中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
  var systemId string
  if env, ok := ctx.Value(runtime.RUNTIME_CTX_ENV).(map[string]string); ok {
    systemId = env["SYSTEM_ID"]
  }

  _, err := db.ExecContext(ctx, `
INSERT INTO users (id, username)
VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING
  `, systemId, "sysmtem_id")
  if err != nil {
    logger.Error("Error: %s", err.Error())
  }

  return nil
}

RPC #

以下示例注册带有标识符 custom_rpc_func_id 的函数。然后可在客户端代码中使用此 ID 发送 RPC 消息,在服务器上执行函数并返回结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func CustomRpcFunc(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
		logger.Info("Payload: %s", payload)

		// "payload" is bytes sent by the client we'll JSON decode it.
		var value interface{}
		if err := json.Unmarshal([]byte(payload), &value); err != nil {
				return "", runtime.NewError("unable to unmarshal payload", 13)
		}

		response, err := json.Marshal(value)
		if err != nil {
				return "", runtime.NewError("unable to marshal payload", 13)
		}

    return string(response), nil
}

// Register as an RPC function, this call should be in InitModule.
if err := initializer.RegisterRpc("custom_rpc_func_id", CustomRpcFunc); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}

Before 挂钩 #

以下代码示例获取当前用户的配置文件,检查元数据。通常假定此元数据采用 JSON 编码,其中有 "{level: 12}"。如果用户级别太低,则会引发一个错误,以阻止在服务器中向前传递“好友添加”消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func BeforeAddFriends(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) (*api.AddFriendsRequest, error) {
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
        return nil, runtime.NewError("missing user id", 13)
    }

    account, err := nk.UsersGetId(ctx, []string{userID})
    if err != nil {
        return nil, runtime.NewError("user not found", 5)
    }

    var metadata map[string]interface{}
    if err := json.Unmarshal([]byte(account.GetUser().GetMetadata()), &metadata); err != nil {
        return nil, runtime.NewError("corrupted user metadata", 13)
    }

    // Let's assume we've stored a user's level in their metadata.
    if level, ok := metadata["level"].(int); !ok || level < 10 {
        return nil, runtime.NewError("must reach level 10 before you can add friends", 9)
    }

    return in, nil
}

// Register as a before hook for the appropriate feature, this call should be in InitModule.
if err := initializer.RegisterBeforeAddFriends(BeforeAddFriends); err != nil {
    logger.Error("Unable to register: %v", err)
    return err
}

After 挂钩 #

下面的示例代码在用户添加好友时将记录写入用户的存储。函数返回的任何数据都将被丢弃。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func AfterAddFriends(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.AddFriendsRequest) error {
    userID, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
    if !ok {
        return runtime.NewError("missing user id", 13)
    }

    value, err := json.Marshal(map[string]interface{}{"user_ids": in.GetIds()})
    if err != nil {
        return runtime.NewError("unable to marshal data", 13)
    }

    if _, err := nk.StorageWrite(ctx, []*runtime.StorageWrite{
        &runtime.StorageWrite{
            Collection: "rewards",
            Key:        "reward",
            UserID:     userID,
            Value:      string(value),
        },
    }); err != nil {
        return runtime.NewError("unable to write to storage engine", 13)
    }

    return nil
}

// Register as an after hook for the appropriate feature, this call should be in InitModule.
if err := initializer.RegisterAfterAddFriends(AfterAddFriends); err != nil {
  logger.Error("Unable to register: %v", err)
  return err
}

示例模块 #

我们以 Pokéapi 作为一个有趣的示例,并建立一个实用模块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import (
		"context"
		"database/sql"
		"encoding/json"
		"errors"
		"io/ioutil"
		"net/http"

		"github.com/heroiclabs/nakama-common/runtime"
)

const apiBaseUrl = "https://pokeapi.co/api/v2"

// All Go modules must have a InitModule function with this exact signature.
func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error {
		// Register the RPC function.
		if err := initializer.RegisterRpc("get_pokemon", GetPokemon); err != nil {
				logger.Error("Unable to register: %v", err)
				return err
		}

		return nil
	}

func LookupPokemon(logger runtime.Logger, name string) (map[string]interface{}, error) {
		resp, err := http.Get(apiBaseUrl + "/pokemon/" + name)

		if err != nil {
				logger.Error("Failed request %v", err.Error())
				return nil, runtime.NewError("unable to retrieve api data", 13)
		}

		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)

		if err != nil {
				logger.Error("Failed to read body %v", err.Error())
				return nil, runtime.NewError("failed to read body", 13)
		}

		if resp.StatusCode >= 400 {
				logger.Error("Failed request %v %v", resp.StatusCode, body)
				return nil, runtime.NewError("failed api request", 13)
		}

		var result map[string]interface{}
		if err := json.Unmarshal(body, &result); err != nil {
      return "", runtime.NewError("unable to unmarshal data", 13)
    }

		return result, nil
}

func GetPokemon(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
		// We'll assume payload was sent as JSON and decode it.
		var input map[string]string
		if err := json.Unmarshal([]byte(payload), &input); err != nil {
      return "", runtime.NewError("unable to unmarshal payload", 13)
    }

		result, err := LookupPokemon(logger, input["PokemonName"])

		if err != nil {
				return "", runtime.NewError("unable to find pokemon", 5)
		}

		response, err := json.Marshal(result)

		if err != nil {
				return "", runtime.NewError("unable to marshal response", 13)
		}

		return string(response), nil
}

现在,您可以从客户端对 Pokémon 进行 RPC 调用:

Client
1
2
3
curl "http://127.0.0.1:7350/v2/rpc/get_pokemon" \
  -H 'authorization: Bearer <session token>'
  -d '"{\"PokemonName\": \"dragonite\"}"'
Client
1
2
3
4
const payload = { "PokemonName": "dragonite"};
const rpcid = "get_pokemon";
const pokemonInfo = await client.rpc(session, rpcid, payload);
console.log("Retrieved pokemon info: %o", pokemonInfo);
Client
1
2
3
4
var payload = JsonWriter.ToJson(new { PokemonName = "dragonite" });
var rpcid = "get_pokemon";
var pokemonInfo = await client.RpcAsync(session, rpcid, payload);
System.Console.WriteLine("Retrieved pokemon info: {0}", pokemonInfo);
Client
1
2
3
4
5
6
7
8
auto successCallback = [](const NRpc& rpc)
{
  	std::cout << "Retrieved pokemon info: " << rpc.payload << std::endl;
};

string payload = "{ \"PokemonName\": \"dragonite\" }";
string rpcid = "get_pokemon";
client->rpc(session, rpcid, payload, successCallback);
Client
1
2
3
4
5
6
Map<String, String> payloadData = new HashMap<>();
payloadData.put("PokemonName", "dragonite");
String payload = new Gson().toJson(payloadData, payloadData.getClass());
String rpcid = "get_pokemon";
Rpc pokemonInfo = client.rpc(session, rpcid, payload);
System.out.format("Retrieved pokemon info: %s", pokemonInfo.getPayload());
Client
1
2
3
4
5
6
7
var payload = {"PokemonName": "dragonite"}
var rpc_id = "get_pokemon"
var pokemon_info : NakamaAPI.ApiRpc = yield(client.rpc_async(session, rpc_id, JSON.print(payload)), "completed")
if pokemon_info.is_exception():
	print("An error occurred: %s" % pokemon_info)
	return
print("Retrieved pokemon info: %s" % [parse_json(pokemon_info.payload)])
Client
1
2
3
4
5
6
7
8
POST /v2/rpc/get_pokemon
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "PokemonName": "dragonite"
}
Client
1
2
3
4
local payload = { PokemonName = "dragonite"}
local rpcid = "get_pokemon"
local pokemon_info = client.rpc_func(rpcid, json.encode(payload)) 
pprint("Retrieved pokemon info:", pokemon_info)