-
Notifications
You must be signed in to change notification settings - Fork 4
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
- Simple-game-client app connects to port using a basic TCP connection | ||||||
- App periodically sends messages and displays anything it receives from the connection. |
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. |
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 |
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= |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick, worth using |
||
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
OS X became macOS a few years ago |
||||||||||
export MP_ACCESS_KEY=9ff2af788834439b83ae6692f34ea5e5 | ||||||||||
export MP_SECRET_KEY=6323354e200a451dba319bc1b98ade59 | ||||||||||
|
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. |
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 | ||
) |
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= |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Product name |
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.