Skip to content

feat: Sample matchmaker and gameclient #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion Third Party Notices.md
Original file line number Diff line number Diff line change
@@ -22,4 +22,20 @@ Component Name: stretchr/testify

License Type: "MIT"

[logrus License](https://github.com/stretchr/testify/blob/master/LICENSE)
[testify License](https://github.com/stretchr/testify/blob/master/LICENSE)

---

Component Name: google/uuid

License Type: "BSD 3-clause"

[google uuid License](https://github.com/google/uuid/blob/master/LICENSE)

---

Component Name: caarlos0/env

License Type: "MIT"

[caarlos0 env License](https://github.com/caarlos0/env/blob/main/LICENSE.md)
15 changes: 15 additions & 0 deletions simple-game-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Simple Game Client

This is a very simple game client, designed to be used with the simple-matchmaker.

This app is designed to provide the simplest complete example of using the Multiplay. It uses a very simple matchmaker
Copy link
Contributor

@eth0net eth0net Jan 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This app is designed to provide the simplest complete example of using the Multiplay. It uses a very simple matchmaker
This app is designed to provide the simplest complete example of using the [Unity Multiplay platform](https://unity.com/products/multiplay). It uses [simple matchmaker](../simple-matchmaker)

which is designed to demonstrate flows that need to be made and is not designed for production use.

## Expected flow:

- Simple-game-client app starts
- Creates a Player UUID unique for the game client run.
- Repeatedly call the simple-matchmaker `/player` endpoint with the player UUID
- Eventually the endpoint will return an IP and Port to connecto
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Eventually the endpoint will return an IP and Port to connecto
- Eventually, the endpoint will return an IP and Port to connect to

- Simple-game-client app connects to port using a basic TCP connection
- App periodically sends messages and displays anything it receives from the connection.
8 changes: 8 additions & 0 deletions simple-game-client/assets/help_en.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

Unity Simple Game Client Example

This sample represents your game client which will be distributed to players. Here it is a very simple client
which connects to the matchmaker and asks for a game. When the matchmaker receives enough players and has allocated
a match it will then tell this client where to connect to.

Once connected this game client will stay connected until the match end, and then exit.
10 changes: 10 additions & 0 deletions simple-game-client/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/Unity-Technologies/multiplay-examples/simple-game-client

go 1.17

require (
github.com/Unity-Technologies/multiplay-examples/simple-matchmaker v0.0.0
github.com/google/uuid v1.3.0
)

replace github.com/Unity-Technologies/multiplay-examples/simple-matchmaker => ../simple-matchmaker
15 changes: 15 additions & 0 deletions simple-game-client/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
160 changes: 160 additions & 0 deletions simple-game-client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package main

import (
"bytes"
_ "embed"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"sync"
"time"

"github.com/Unity-Technologies/multiplay-examples/simple-matchmaker/pkg/matchmaker"
"github.com/google/uuid"
)

var (
//go:embed assets/help_en.txt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the use of go embed 😃

helpEn string
)

func main() {
showHelp := flag.Bool("help", false, "Display help")
matchmakerURL := flag.String("matchmaker", "http://localhost:8085", "The URL where the sample matchmaker is running")
flag.Parse()

if *showHelp {
displayHelp()
return
}

fmt.Println("Starting to find a match")

if err := matchmake(*matchmakerURL); err != nil {
fmt.Println(fmt.Errorf("match: %w", err))
}
fmt.Println("Ending Match")
}

func matchmake(matchmakerURL string) (err error) {
// Create a unique player id so the matchmaker can associate requests with us.
playerID := uuid.New().String()

matchInfo := &matchmaker.MatchInfo{}
for {

fmt.Printf("Asking matchmaker about match for us (playerid: %s)\n", playerID)

// Repeatedly call the matchmakers player join endpoint
matchInfo, err = requestPlayerJoin(playerID, matchmakerURL)
if err != nil {
return err
}

// There are three stages here in matchInfo.
// MatchedPlayers is true - Matchmaker has found players to put together
// AllocationUUID is non-empty - Matchmaker has requested allocation from api
// IP address is non-empty - Matchmaker has been told the game is running here
// We only care about the last one here.
if matchInfo.IP != "" {
// We got a match! Break out of the loop and play the match.
fmt.Println("Matchmaker found us a match")
break
}

fmt.Println("Matchmaker did not have a match ready")
<-time.After(time.Second)
}

fmt.Printf("Connecting to match:\n")
fmt.Printf(" - Allocation UUID: %s:%d\n", matchInfo.AllocationUUID, matchInfo.Port)
fmt.Printf(" - Address: %s:%d\n", matchInfo.IP, matchInfo.Port)
fmt.Printf(" - Other players:\n")
for _, pl := range matchInfo.Players {
fmt.Printf(" - - %s - %s\n", pl.PlayerUUID, pl.IP)
}

tcpAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", matchInfo.IP, matchInfo.Port))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, worth using net.JoinHostPort instead for future support of IPv6?

if err != nil {
return fmt.Errorf("resolve gameserver address: %w", err)
}

conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
return fmt.Errorf("dial gameserver: %w", err)
}

wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for {
_, err = conn.Write([]byte(fmt.Sprintf("Player checking in: %s\n", playerID)))
if err != nil {
fmt.Printf("could not send to server: giving up: %s\n", err.Error())
return
}
<-time.After(time.Second)
}
}()

wg.Add(1)
go func() {
defer wg.Done()
for {
if err = conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
fmt.Printf("could not set read deadline for server: giving up: %s\n", err.Error())
}
content, err := ioutil.ReadAll(conn)
if err != nil && !os.IsTimeout(err) {
fmt.Printf("could not send to server: giving up: %s\n", err.Error())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be receive from server?

return
}
fmt.Println(string(content))
<-time.After(time.Millisecond * 200)
}
}()
wg.Wait()

fmt.Println("Could not read or write to server. Match likely ended.")
return nil
}

func requestPlayerJoin(playerID string, matchmakerURL string) (*matchmaker.MatchInfo, error) {
player := matchmaker.PlayerInfo{
PlayerUUID: playerID,
}

content, err := json.Marshal(player)
if err != nil {
return nil, fmt.Errorf("marshal player info: %w", err)
}

req, err := http.NewRequest(http.MethodGet, matchmakerURL+"/player", bytes.NewBuffer(content))
if err != nil {
return nil, fmt.Errorf("matchmaker player request: %w", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("mathmaker send player request: %w", err)
}

matchInfo := matchmaker.MatchInfo{}
err = json.NewDecoder(resp.Body).Decode(&matchInfo)
if err != nil {
return nil, fmt.Errorf("decode match info: %w", err)
}

return &matchInfo, err
}

func displayHelp() {
fmt.Println(helpEn)
fmt.Println("Arguments:")
flag.PrintDefaults()
}
21 changes: 21 additions & 0 deletions simple-matchmaker/assets/help_en.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

Unity Simple Matchmaker Example

To use multiplay, most games need a matchmaker which groups players.
For the most streamlined experience we recommend that you use the unity matchmaker to do this, but you may use an
Comment on lines +4 to +5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To use multiplay, most games need a matchmaker which groups players.
For the most streamlined experience we recommend that you use the unity matchmaker to do this, but you may use an
To use Multiplay, most games need a matchmaker which groups players.
For the most streamlined experience we recommend that you use the Unity Matchmaker to do this, but you may use an

Should probably capitalise company and product names

alternative or your own custom implementation.

This sample provides an incredibly simple matchmaker to get you started locally. It will group incoming players
together and when enough have joined it will request a match from either its multiplay mock, or the real multiplay API.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
together and when enough have joined it will request a match from either its multiplay mock, or the real multiplay API.
together and when enough have joined it will request a match from either its Multiplay mock, or the real Multiplay API.

Same as above


Authentication is provided through environmental variables. You may set this in your system environmental variables
or may temporarily set this for your terminal session as shown below.

Windows Example:
set MP_ACCESS_KEY=9ff2af788834439b83ae6692f34ea5e5
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are randomly generated

set MP_SECRET_KEY=6323354e200a451dba319bc1b98ade59

For example on UNIX Style OS (OSX, Linux, BSD):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For example on UNIX Style OS (OSX, Linux, BSD):
For example on UNIX Style OS (macOS, Linux, BSD):

OS X became macOS a few years ago

export MP_ACCESS_KEY=9ff2af788834439b83ae6692f34ea5e5
export MP_SECRET_KEY=6323354e200a451dba319bc1b98ade59

8 changes: 8 additions & 0 deletions simple-matchmaker/assets/standalone_en.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

The matchmaker is running in standalone mode.

In this mode it will still matchmake players in groups. however the match will send players to a TCP mirror running
inside this simple matchmaker.

This is designed to allow you to play around with how this matchmaker works fully standalone and is not representative
of a full allocation/deallocation flow you would see in a real game.
17 changes: 17 additions & 0 deletions simple-matchmaker/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module github.com/Unity-Technologies/multiplay-examples/simple-matchmaker

go 1.17

require (
github.com/caarlos0/env v3.5.0+incompatible
github.com/google/uuid v1.3.0
github.com/stretchr/testify v1.7.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
23 changes: 23 additions & 0 deletions simple-matchmaker/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 changes: 4 additions & 0 deletions simple-matchmaker/internal/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Simple Matchmaker Multiplay Client Library

This is a temporary package which provides multiplay API calls. This is a temporary package until the official
multiplay SDK library becomes available.
Comment on lines +3 to +4
Copy link
Contributor

@eth0net eth0net Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This is a temporary package which provides multiplay API calls. This is a temporary package until the official
multiplay SDK library becomes available.
This is a temporary package which provides Multiplay API calls. This is a temporary package until the official
Multiplay SDK library becomes available.

Product name

68 changes: 68 additions & 0 deletions simple-matchmaker/internal/client/allocate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package mpclient

import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
)

// AllocateResponse contains the response from the api
type AllocateResponse struct {
ProfileID int64
UUID string
RegionID string
Created string
Error string
}

type allocateResponseWrapper struct {
Success bool
Allocation AllocateResponse
}

// Allocate allocates using the multiplay api
func (m *multiplayClient) Allocate(fleet, region string, profile int64, uuid string) (*AllocateResponse, error) {
fmt.Println("Allocating", m.baseURL)
urlStr := fmt.Sprintf("%s/cfp/v1/server/allocate", m.baseURL)
u, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("parse url %s", urlStr)
}

params := url.Values{}
params.Add("regionid", region)
params.Add("profileid", strconv.FormatInt(profile, 10))
params.Add("uuid", uuid)
u.RawQuery = params.Encode()

req, err := http.NewRequest(http.MethodPost, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("allocate new request")
}

fmt.Println("Access:", m.accessKey, "Secret:", m.secretKey)
req.SetBasicAuth(m.accessKey, m.secretKey)

res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("send allocate request: %w", err)
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("allocate call status not ok: %d", res.StatusCode)
}

var ar allocateResponseWrapper
if err := json.NewDecoder(res.Body).Decode(&ar); err != nil {
return nil, fmt.Errorf("decode allocate response: %w", err)
}

if !ar.Success {
return nil, fmt.Errorf("allocation request failed: %+v", ar)
}

return &ar.Allocation, nil
}
Loading