코드 샘플 #

이 페이지는 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를 생성하고 사용자 지정 SQL 쿼리는 이 ID를 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
}

사전 후크 #

아래의 코드 예시는 현재 사용자의 프로필을 가져와서 "{level: 12}"(으)로 인코딩된 JSON으로 추정되는 메타데이터를 확인합니다. 사용자의 수준이 낮은 경우, 오류가 발생하여 친구 추가 메시지가 서버에서 전달되지 않도록 합니다:

 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
}

사후 후크 #

아래의 예시 코드는 사용자가 친구를 추가할 때 사용자의 저장소에 레코드를 작성합니다. 함수에서 반환되는 모든 데이터는 삭제됩니다.

 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)

Related Pages