การเริ่มต้นใช้งาน Cloud Spanner กับการพัฒนาเกม

การเริ่มต้นใช้งาน Cloud Spanner กับการพัฒนาเกม

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ พ.ค. 1, 2023
account_circleเขียนโดย Derek Downey, Aalok Muley

1 บทนำ

Cloud Spanner เป็นบริการฐานข้อมูลเชิงสัมพันธ์ที่รองรับการปรับขนาดในวงกว้างและกระจายไปทั่วโลกที่มีการจัดการครบวงจร ซึ่งมอบธุรกรรม ACID และความหมายของ SQL โดยไม่สูญเสียประสิทธิภาพการทำงานและความพร้อมใช้งานสูง

ฟีเจอร์เหล่านี้ทำให้ Spanner มีความเหมาะสมกับสถาปัตยกรรมของเกมที่ต้องการสร้างฐานผู้เล่นทั่วโลก หรือมีความกังวลเกี่ยวกับความสอดคล้องของข้อมูล

ในห้องทดลองนี้ คุณจะสร้างบริการ Go 2 บริการที่โต้ตอบกับฐานข้อมูล Spanner ในระดับภูมิภาคเพื่อให้ผู้เล่นลงชื่อสมัครใช้และเริ่มเล่นได้

413fdd57bb0b68bc.png

ถัดไป คุณจะต้องสร้างข้อมูลโดยใช้ประโยชน์จากเฟรมเวิร์กการโหลด Python Locust.io เพื่อจำลองให้ผู้เล่นลงชื่อสมัครใช้และเล่นเกม จากนั้นค้นหา Spanner เพื่อระบุจำนวนผู้เล่นที่เล่น และสถิติบางอย่างเกี่ยวกับผู้เล่น เกมที่ชนะเทียบกับเกมที่เล่น

ขั้นตอนสุดท้าย คุณจะล้างทรัพยากรที่สร้างขึ้นในห้องทดลองนี้

สิ่งที่คุณจะสร้าง

ในห้องทดลองนี้ คุณจะทำสิ่งต่อไปนี้ได้

  • สร้างอินสแตนซ์ Spanner
  • ทำให้บริการโปรไฟล์ที่เขียนใน Go ใช้งานได้เพื่อจัดการการลงชื่อสมัครใช้โปรแกรมเล่น
  • ใช้บริการจับคู่ที่เขียนใน Go เพื่อกำหนดผู้เล่นให้เล่นเกม กำหนดผู้ชนะ และอัปเดตผู้เล่น สถิติเกม

สิ่งที่คุณจะได้เรียนรู้

  • วิธีตั้งค่าอินสแตนซ์ Cloud Spanner
  • วิธีสร้างฐานข้อมูลและสคีมาเกม
  • วิธีทำให้แอป Go ใช้งานได้เพื่อให้ทำงานกับ Cloud Spanner
  • วิธีสร้างข้อมูลโดยใช้ Locust
  • วิธีค้นหาข้อมูลใน Cloud Spanner เพื่อตอบคำถามเกี่ยวกับเกมและผู้เล่น

สิ่งที่คุณต้องมี

  • โปรเจ็กต์ Google Cloud ที่เชื่อมต่อกับบัญชีสำหรับการเรียกเก็บเงิน
  • เว็บเบราว์เซอร์ เช่น Chrome หรือ Firefox

2 การตั้งค่าและข้อกำหนด

สร้างโปรเจ็กต์

หากยังไม่มีบัญชี Google (Gmail หรือ Google Apps) คุณต้องสร้างบัญชีก่อน ลงชื่อเข้าใช้คอนโซล Google Cloud Platform ( console.cloud.google.com) และสร้างโปรเจ็กต์ใหม่

หากคุณมีโปรเจ็กต์อยู่แล้ว ให้คลิกเมนูแบบเลื่อนลงสำหรับการเลือกโปรเจ็กต์ที่ด้านซ้ายบนของคอนโซล

6c9406d9b014760.png

แล้วคลิก "โปรเจ็กต์ใหม่" ในกล่องโต้ตอบที่ปรากฏขึ้นเพื่อสร้างโปรเจ็กต์ใหม่ ดังนี้

949d83c8a4ee17d9.png

หากคุณยังไม่มีโปรเจ็กต์ คุณจะเห็นกล่องโต้ตอบลักษณะนี้ให้สร้างโปรเจ็กต์แรก

870a3cbd6541ee86.png

กล่องโต้ตอบการสร้างโปรเจ็กต์ที่ตามมาจะให้คุณป้อนรายละเอียดของโปรเจ็กต์ใหม่:

6a92c57d3250a4b3.png

จดจำรหัสโปรเจ็กต์ ซึ่งเป็นชื่อที่ไม่ซ้ำกันในโปรเจ็กต์ Google Cloud ทั้งหมด (ระบบใช้ชื่อด้านบนนี้ไปแล้ว และจะใช้ไม่ได้ ขออภัย) โดยจะเรียกใน Codelab ว่า PROJECT_ID ในภายหลัง

ขั้นตอนถัดไป คุณจะต้องเปิดใช้การเรียกเก็บเงินใน Developers Console เพื่อใช้ทรัพยากร Google Cloud และเปิดใช้ Cloud Spanner API หากยังไม่ได้ดำเนินการ

15d0ef27a8fbab27.png

การใช้งาน Codelab นี้น่าจะมีค่าใช้จ่ายไม่เกิน 2-3 ดอลลาร์ แต่อาจมากกว่านี้หากคุณตัดสินใจใช้ทรัพยากรเพิ่มหรือปล่อยให้ทำงาน (ดูส่วน "ล้างข้อมูล" ในตอนท้ายของเอกสารนี้) ดูข้อมูลเกี่ยวกับราคาของ Google Cloud Spanner ได้ที่นี่

ผู้ใช้ใหม่ของ Google Cloud Platform จะมีสิทธิ์ทดลองใช้ฟรี$300 ซึ่งจะทำให้ Codelab นี้ไม่มีค่าใช้จ่ายทั้งหมด

การตั้งค่า Google Cloud Shell

แม้ว่า Google Cloud และ Spanner จะทำงานจากระยะไกลได้จากแล็ปท็อป แต่ใน Codelab นี้ เราจะใช้ Google Cloud Shell ซึ่งเป็นสภาพแวดล้อมแบบบรรทัดคำสั่งที่ทำงานในระบบคลาวด์

เครื่องเสมือนแบบ Debian นี้เต็มไปด้วยเครื่องมือการพัฒนาทั้งหมดที่คุณต้องการ โดยมีไดเรกทอรีหลักขนาด 5 GB ที่ทำงานอย่างต่อเนื่องใน Google Cloud ซึ่งจะช่วยเพิ่มประสิทธิภาพของเครือข่ายและการตรวจสอบสิทธิ์ได้อย่างมาก ซึ่งหมายความว่าสิ่งที่คุณต้องมีสำหรับ Codelab นี้คือเบราว์เซอร์ (ใช่แล้ว ทั้งหมดนี้ทำงานได้บน Chromebook)

  1. หากต้องการเปิดใช้งาน Cloud Shell จาก Cloud Console เพียงคลิกเปิดใช้งาน Cloud Shell gcLMt5IuEcJJNnMId-Bcz3sxCd0rZn7IzT_r95C8UZeqML68Y1efBG_B0VRp7hc7qiZTLAF-TXD7SsOadxn8uadgHhaLeASnVS3ZHK39eOlKJOgj9SJua_oeGhMxRrbOg3qigddS2A (การจัดสรรและเชื่อมต่อกับสภาพแวดล้อมซึ่งจะใช้เวลาเพียงไม่นาน)

JjEuRXGg0AYYIY6QZ8d-66gx_Mtc-_jDE9ijmbXLJSAXFvJt-qUpNtsBsYjNpv2W6BQSrDc1D-ARINNQ-1EkwUhz-iUK-FUCZhJ-NtjvIEx9pIkE-246DomWuCfiGHK78DgoeWkHRw

Screen Shot 14-06-2017 เวลา 22.13.43 น.

เมื่อเชื่อมต่อกับ Cloud Shell แล้ว คุณควรเห็นว่าได้รับการตรวจสอบสิทธิ์แล้วและโปรเจ็กต์ได้รับการตั้งค่าเป็น PROJECT_ID แล้ว

gcloud auth list

เอาต์พุตจากคำสั่ง

Credentialed accounts:
 
- <myaccount>@<mydomain>.com (active)
gcloud config list project

เอาต์พุตจากคำสั่ง

[core]
project
= <PROJECT_ID>

หากโปรเจ็กต์ไม่ได้ตั้งค่าไว้ด้วยเหตุผลบางประการ ให้ใช้คำสั่งต่อไปนี้

gcloud config set project <PROJECT_ID>

กำลังมองหา PROJECT_ID อยู่ใช่ไหม ตรวจสอบรหัสที่คุณใช้ในขั้นตอนการตั้งค่าหรือดูในแดชบอร์ด Cloud Console

158fNPfwSxsFqz9YbtJVZes8viTS3d1bV4CVhij3XPxuzVFOtTObnwsphlm6lYGmgdMFwBJtc-FaLrZU7XHAg_ZYoCrgombMRR3h-eolLPcvO351c5iBv506B3ZwghZoiRg6cz23Qw

Cloud Shell ยังตั้งค่าตัวแปรสภาพแวดล้อมโดยค่าเริ่มต้นด้วย ซึ่งอาจเป็นประโยชน์เมื่อคุณเรียกใช้คำสั่งในอนาคต

echo $GOOGLE_CLOUD_PROJECT

เอาต์พุตจากคำสั่ง

<PROJECT_ID>

ดาวน์โหลดโค้ด

คุณสามารถดาวน์โหลดโค้ดสำหรับห้องทดลองนี้ได้ใน Cloud Shell ข้อมูลนี้อิงตามรุ่น v0.1.0 ดังนั้นโปรดตรวจสอบแท็กดังต่อไปนี้

git clone https://github.com/cloudspannerecosystem/spanner-gaming-sample.git
cd spanner
-gaming-sample/

# Check out v0.1.0 release
git checkout tags
/v0.1.0 -b v0.1.0-branch

เอาต์พุตจากคำสั่ง

Cloning into 'spanner-gaming-sample'...
*snip*
Switched to a new branch 'v0.1.0-branch'

ตั้งค่าเครื่องมือสร้างโหลด Locust

Locust เป็นเฟรมเวิร์กการทดสอบโหลด Python ที่มีประโยชน์ในการทดสอบปลายทาง API ของ REST ใน Codelab นี้ เรามีการทดสอบโหลด 2 แบบใน "โปรแกรมสร้าง" ไดเรกทอรีที่เราจะเน้น ได้แก่

  • authentication_server.py: มีงานสร้างผู้เล่น และให้ผู้เล่นแบบสุ่มเลียนแบบการค้นหาจุดเดียว
  • match_server.py: มีงานสำหรับสร้างเกมและปิดเกม การสร้างเกมจะกำหนดผู้เล่นแบบสุ่ม 100 คนที่ไม่ได้เล่นเกมอยู่ การปิดเกมจะอัปเดตสถิติ games_played และ games_won และอนุญาตให้กำหนดผู้เล่นเหล่านั้นในเกมในอนาคต

คุณจะต้องมี Python 3.7 ขึ้นไปเพื่อให้ Locust ทำงานใน Cloud Shell Cloud Shell มาพร้อมกับ Python 3.9 คุณจึงไม่ต้องดำเนินการใดๆ นอกจากตรวจสอบเวอร์ชัน

python -V

เอาต์พุตจากคำสั่ง

Python 3.9.12

คุณสามารถติดตั้งข้อกำหนดของ Locust ได้แล้ว

pip3 install -r requirements.txt

เอาต์พุตจากคำสั่ง

Collecting locust==2.11.1
*snip*
Successfully installed ConfigArgParse-1.5.3 Flask-BasicAuth-0.2.0 Flask-Cors-3.0.10 brotli-1.0.9 gevent-21.12.0 geventhttpclient-2.0.2 greenlet-1.1.3 locust-2.11.1 msgpack-1.0.4 psutil-5.9.2 pyzmq-22.3.0 roundrobin-0.0.4 zope.event-4.5.0 zope.interface-5.4.0

ตอนนี้ ให้อัปเดต PATH เพื่อให้ระบบพบไบนารี locust ที่ติดตั้งใหม่ ดังนี้

PATH=~/.local/bin":$PATH"
which locust

เอาต์พุตจากคำสั่ง

/home/<user>/.local/bin/locust

สรุป

ในขั้นตอนนี้ คุณจะตั้งค่าโปรเจ็กต์ของคุณหากยังไม่มี เปิดใช้งาน Cloud Shell และดาวน์โหลดโค้ดสำหรับห้องทดลองนี้

สุดท้าย คุณตั้งค่า Locust สำหรับการสร้างภาระงานภายหลังใน Lab

รายการถัดไป

ต่อไปคุณจะตั้งค่าอินสแตนซ์และฐานข้อมูล Cloud Spanner

3 สร้างอินสแตนซ์และฐานข้อมูล Spanner

สร้างอินสแตนซ์ Spanner

ในขั้นตอนนี้ เราจะตั้งค่าอินสแตนซ์ Spanner สำหรับ Codelab ค้นหารายการ Spanner 1a6580bd3d3e6783.pngในเมนูแฮมเบอร์เกอร์ 3129589f7bc9e5ce.png ด้านบนซ้าย หรือค้นหา Spanner โดยกด "/" และพิมพ์ "SPANer"

36e52f8df8e13b99.png

จากนั้นคลิก 95269e75bc8c3e4d.png และกรอกแบบฟอร์มโดยป้อนชื่ออินสแตนซ์ cloudspanner-gaming สำหรับอินสแตนซ์ เลือกการกำหนดค่า (เลือกอินสแตนซ์ระดับภูมิภาค เช่น us-central1) แล้วกำหนดจำนวนโหนด สำหรับ Codelab นี้ เราจะใช้เพียง 500 processing units เท่านั้น

สุดท้ายแต่ไม่ท้ายสุด ให้คลิก "สร้าง" และคุณก็มีอินสแตนซ์ Cloud Spanner อยู่แล้วภายในไม่กี่วินาที

4457c324c94f93e6.png

สร้างฐานข้อมูลและสคีมา

คุณสร้างฐานข้อมูลได้เมื่ออินสแตนซ์ทำงาน Spanner ช่วยให้สามารถใช้ฐานข้อมูลได้หลายฐานข้อมูลในอินสแตนซ์เดียว

ฐานข้อมูลคือที่ที่คุณกำหนดสคีมา อีกทั้งยังควบคุมผู้ที่มีสิทธิ์เข้าถึงฐานข้อมูล ตั้งค่าการเข้ารหัสที่กำหนดเอง กำหนดค่าเครื่องมือเพิ่มประสิทธิภาพ และตั้งค่าระยะเวลาเก็บรักษาได้ด้วย

ในอินสแตนซ์หลายภูมิภาค คุณยังกำหนดค่าตัวแปรเริ่มต้นเริ่มต้นได้ด้วย อ่านเพิ่มเติมเกี่ยวกับฐานข้อมูลบน Spanner

สำหรับ Code-lab นี้ คุณจะสร้างฐานข้อมูลที่มีตัวเลือกเริ่มต้น และให้สคีมาในขณะที่สร้าง

ห้องทดลองนี้จะสร้างตาราง 2 ตาราง ได้แก่ ผู้เล่นและเกม

77651ac12e47fe2a.png

ผู้เล่นจะเข้าร่วมการแข่งขันหลายรายการได้ในช่วงเวลาหนึ่งๆ แต่จะเข้าร่วมได้เพียงครั้งละ 1 เกม ผู้เล่นยังมีสถิติเป็นประเภทข้อมูล JSON เพื่อติดตามสถิติที่น่าสนใจ เช่น games_played และ games_won เนื่องจากอาจมีการเพิ่มสถิติอื่นๆ เข้ามาภายหลัง นี่จึงเป็นคอลัมน์ที่ไม่มีสคีมาสำหรับผู้เล่น

เกมจะติดตามผู้เล่นที่เข้าร่วมโดยใช้ประเภทข้อมูล ARRAY ของ Spanner ระบบจะไม่ป้อนข้อมูลแอตทริบิวต์ผู้ชนะและรายการที่เสร็จสิ้นของเกมจนกว่าเกมจะปิดลง

มีคีย์นอก 1 คีย์ที่ช่วยให้แน่ใจว่า current_game ของผู้เล่นเป็นเกมที่ถูกต้อง

สร้างฐานข้อมูลด้วยการคลิก "สร้างฐานข้อมูล" ในภาพรวมของอินสแตนซ์

a820db6c4a4d6f2d.png

จากนั้นกรอกรายละเอียด ตัวเลือกที่สำคัญคือชื่อฐานข้อมูลและภาษา ในตัวอย่างนี้ เราตั้งชื่อฐานข้อมูลว่า sample-game และเลือกภาษา SQL มาตรฐานของ Google

สำหรับสคีมา ให้คัดลอกและวาง DDL นี้ลงในช่อง

CREATE TABLE games (
  gameUUID STRING
(36) NOT NULL,
  players ARRAY
<STRING(36)> NOT NULL,
  winner STRING
(36),
  created TIMESTAMP
,
  finished TIMESTAMP
,
) PRIMARY KEY(gameUUID);

CREATE TABLE players
(
  playerUUID STRING
(36) NOT NULL,
  player_name STRING
(64) NOT NULL,
  email STRING
(MAX) NOT NULL,
  password_hash BYTES
(60) NOT NULL,
  created TIMESTAMP
,
  updated TIMESTAMP
,
  stats JSON
,
  account_balance NUMERIC NOT NULL DEFAULT
(0.00),
  is_logged_in BOOL
,
  last_login TIMESTAMP
,
  valid_email BOOL
,
  current_game STRING
(36),
  FOREIGN KEY
(current_game) REFERENCES games (gameUUID),
) PRIMARY KEY(playerUUID);

CREATE UNIQUE INDEX
PlayerAuthentication ON players(email) STORING (password_hash);

CREATE INDEX
PlayerGame ON players(current_game);

CREATE UNIQUE INDEX
PlayerName ON players(player_name);

จากนั้นคลิกปุ่มสร้าง และรอสักครู่เพื่อให้ระบบสร้างฐานข้อมูลของคุณ

หน้าสร้างฐานข้อมูลควรมีลักษณะดังนี้

d39d358dc7d32939.png

ตอนนี้คุณต้องตั้งค่าตัวแปรสภาพแวดล้อมบางอย่างใน Cloud Shell เพื่อใช้ในภายหลังในห้องทดลองโค้ด ดังนั้น ให้จดรหัสอินสแตนซ์ และตั้งค่า INSTANCE_ID และ DATABASE_ID ใน Cloud Shell

f6f98848d3aea9c.png

export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game

สรุป

ในขั้นตอนนี้ คุณจะได้สร้างอินสแตนซ์ Spanner และฐานข้อมูลเกมตัวอย่าง คุณยังกำหนดสคีมาที่เกมตัวอย่างนี้ใช้ได้อีกด้วย

รายการถัดไป

ถัดไป คุณจะทำให้บริการโปรไฟล์ใช้งานได้เพื่อให้ผู้เล่นลงชื่อสมัครใช้เพื่อเล่นเกมได้

4 ทำให้บริการโปรไฟล์ใช้งานได้

ภาพรวมของบริการ

บริการโปรไฟล์คือ REST API ที่เขียนใน Go ซึ่งใช้ประโยชน์จากเฟรมเวิร์ก Gin

4fce45ee6c858b3e.png

ผู้เล่นสามารถลงชื่อสมัครใช้เพื่อเล่นเกมได้ใน API นี้ ซึ่งสร้างโดยคำสั่ง POST พื้นฐานที่ยอมรับชื่อโปรแกรมเล่น อีเมล และรหัสผ่าน รหัสผ่านจะเข้ารหัสด้วย bcrypt และระบบจะเก็บแฮชไว้ในฐานข้อมูล

อีเมลจะถือว่าเป็นตัวระบุที่ไม่ซ้ำกัน ในขณะที่ player_name จะใช้เพื่อแสดงสำหรับเกม

ปัจจุบัน API นี้ไม่สามารถจัดการการเข้าสู่ระบบ แต่คุณสามารถดำเนินการเพิ่มเติมให้กับคุณได้

ไฟล์ ./src/golang/profile-service/main.go สำหรับบริการโปรไฟล์จะแสดงปลายทางหลัก 2 รายการดังนี้

func main() {
   configuration
, _ := config.NewConfig()

   router
:= gin.Default()
   router
.SetTrustedProxies(nil)

   router
.Use(setSpannerConnection(configuration))

   router
.POST("/players", createPlayer)
   router
.GET("/players", getPlayerUUIDs)
   router
.GET("/players/:id", getPlayerByID)

   router
.Run(configuration.Server.URL())
}

และโค้ดสำหรับปลายทางเหล่านั้นจะส่งเส้นทางไปยังโมเดลโปรแกรมเล่น

func getPlayerByID(c *gin.Context) {
   
var playerUUID = c.Param("id")

   ctx
, client := getSpannerConnection(c)

   player
, err := models.GetPlayerByUUID(ctx, client, playerUUID)
   
if err != nil {
       c
.IndentedJSON(http.StatusNotFound, gin.H{"message": "player not found"})
       
return
   
}

   c
.IndentedJSON(http.StatusOK, player)
}

func createPlayer
(c *gin.Context) {
   
var player models.Player

   
if err := c.BindJSON(&player); err != nil {
       c
.AbortWithError(http.StatusBadRequest, err)
       
return
   
}

   ctx
, client := getSpannerConnection(c)
   err
:= player.AddPlayer(ctx, client)
   
if err != nil {
       c
.AbortWithError(http.StatusBadRequest, err)
       
return
   
}

   c
.IndentedJSON(http.StatusCreated, player.PlayerUUID)
}

สิ่งแรกที่บริการทำคือการตั้งค่าการเชื่อมต่อ Spanner ซึ่งจะมีการดำเนินการในระดับบริการเพื่อสร้างพูลเซสชันสำหรับบริการ

func setSpannerConnection() gin.HandlerFunc {
   ctx
:= context.Background()
   client
, err := spanner.NewClient(ctx, configuration.Spanner.URL())

   
if err != nil {
       log
.Fatal(err)
   
}

   
return func(c *gin.Context) {
       c
.Set("spanner_client", *client)
       c
.Set("spanner_context", ctx)
       c
.Next()
   
}
}

Player และ PlayerStats มีโครงสร้างที่กำหนดไว้ดังนี้

type Player struct {
   
PlayerUUID      string `json:"playerUUID" validate:"omitempty,uuid4"`
   
Player_name     string `json:"player_name" validate:"required_with=Password Email"`
   
Email           string `json:"email" validate:"required_with=Player_name Password,email"`
   
// not stored in DB
   
Password        string `json:"password" validate:"required_with=Player_name Email"`
   
// stored in DB
   
Password_hash   []byte `json:"password_hash"`                                      
   created         time
.Time
   updated         time
.Time
   
Stats           spanner.NullJSON `json:"stats"`
   
Account_balance big.Rat          `json:"account_balance"`
   last_login      time
.Time
   is_logged_in    
bool
   valid_email    
bool
   
Current_game    string `json:"current_game" validate:"omitempty,uuid4"`
}

type
PlayerStats struct {
   
Games_played spanner.NullInt64 `json:"games_played"`
   
Games_won    spanner.NullInt64 `json:"games_won"`
}

ฟังก์ชันในการเพิ่มโปรแกรมเล่นจะใช้การแทรก DML ภายในธุรกรรม ReadWrite เนื่องจากการเพิ่มผู้เล่นเป็นคำสั่งเดี่ยว ไม่ใช่การแทรกเป็นกลุ่ม ฟังก์ชันจะมีลักษณะดังนี้

func (p *Player) AddPlayer(ctx context.Context, client spanner.Client) error {
   
// Validate based on struct validation rules
   err
:= p.Validate()
   
if err != nil {
       
return err
   
}

   
// take supplied password+salt, hash. Store in user_password
   passHash
, err := hashPassword(p.Password)

   
if err != nil {
       
return errors.New("Unable to hash password")
   
}

   p
.Password_hash = passHash

   
// Generate UUIDv4
   p
.PlayerUUID = generateUUID()

   
// Initialize player stats
   emptyStats
:= spanner.NullJSON{Value: PlayerStats{
       
Games_played: spanner.NullInt64{Int64: 0, Valid: true},
       
Games_won:    spanner.NullInt64{Int64: 0, Valid: true},
   
}, Valid: true}

   
// insert into spanner
   _
, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       stmt
:= spanner.Statement{
           SQL
: `INSERT players (playerUUID, player_name, email, password_hash, created, stats) VALUES
                   (@playerUUID, @playerName, @email, @passwordHash, CURRENT_TIMESTAMP(), @pStats)
           `
,
           
Params: map[string]interface{}{
               
"playerUUID":   p.PlayerUUID,
               
"playerName":   p.Player_name,
               
"email":        p.Email,
               
"passwordHash": p.Password_hash,
               
"pStats":       emptyStats,
           
},
       
}

       _
, err := txn.Update(ctx, stmt)
       
return err
   
})
   
if err != nil {
       
return err
   
}
   
// return empty error on success
   
return nil
}

หากต้องการดึงข้อมูลโปรแกรมเล่นโดยอิงตาม UUID ของโปรแกรมเล่นดังกล่าว ระบบจะออกการอ่านอย่างง่าย การดำเนินการนี้จะเรียก playerUUID, Player_name, email และ stats ของโปรแกรมเล่น

func GetPlayerByUUID(ctx context.Context, client spanner.Client, uuid string) (Player, error) {
   row
, err := client.Single().ReadRow(ctx, "players",
       spanner
.Key{uuid}, []string{"playerUUID", "player_name", "email", "stats"})
   
if err != nil {
       
return Player{}, err
   
}

   player
:= Player{}
   err
= row.ToStruct(&player)

   
if err != nil {
       
return Player{}, err
   
}
   
return player, nil
}

โดยค่าเริ่มต้น ระบบจะกำหนดค่าบริการโดยใช้ตัวแปรสภาพแวดล้อม โปรดดูส่วนที่เกี่ยวข้องของไฟล์ ./src/golang/profile-service/config/config.go

func NewConfig() (Config, error) {
   
*snip*
   
// Server defaults
   viper
.SetDefault("server.host", "localhost")
   viper
.SetDefault("server.port", 8080)

   
// Bind environment variable override
   viper
.BindEnv("server.host", "SERVICE_HOST")
   viper
.BindEnv("server.port", "SERVICE_PORT")
   viper
.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
   viper
.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
   viper
.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")

   
*snip*

   
return c, nil
}

คุณจะเห็นว่าการทำงานเริ่มต้นคือการเรียกใช้บริการใน localhost:8080

เมื่อมีข้อมูลนี้แล้ว ก็ถึงเวลาที่จะเรียกใช้บริการ

เรียกใช้บริการโปรไฟล์

เรียกใช้บริการโดยใช้คำสั่ง go การดำเนินการนี้จะดาวน์โหลดทรัพยากร Dependency และสร้างบริการที่ทำงานอยู่บนพอร์ต 8080 ดังนี้

cd ~/spanner-gaming-sample/src/golang/profile-service
go run
. &

เอาต์พุตจากคำสั่ง:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 
- using env:   export GIN_MODE=release
 
- using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /players                  --> main.createPlayer (4 handlers)
[GIN-debug] GET    /players                  --> main.getPlayerUUIDs (4 handlers)
[GIN-debug] GET    /players/:id              --> main.getPlayerByID (4 handlers)
[GIN-debug] GET    /players/:id/stats        --> main.getPlayerStats (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8080

ทดสอบบริการโดยการออกคำสั่ง curl ดังนี้

curl http://localhost:8080/players \
   
--include \
   
--header "Content-Type: application/json" \
   
--request "POST" \
   
--data '{"email": "test@gmail.com","password": "s3cur3P@ss","player_name": "Test Player"}'

เอาต์พุตจากคำสั่ง:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 18:55:08 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

สรุป

ในขั้นตอนนี้ คุณได้ใช้บริการโปรไฟล์ที่อนุญาตให้ผู้เล่นลงชื่อสมัครใช้เพื่อเล่นเกมของคุณ และคุณได้ทดสอบบริการโดยเรียก POST API เพื่อสร้างผู้เล่นใหม่

ขั้นตอนถัดไป

ในขั้นตอนถัดไป คุณจะต้องใช้บริการจับคู่

5 ติดตั้งใช้งานบริการจับคู่

ภาพรวมของบริการ

บริการจับคู่คือ REST API ที่เขียนขึ้นใน Go ซึ่งใช้ประโยชน์จากเฟรมเวิร์ก Gin

9aecd571df0dcd7c.png

เกมใน API นี้สร้างและปิดไปแล้ว เมื่อสร้างเกม ระบบจะกำหนดผู้เล่น 10 คนที่ไม่ได้เล่นเกมอยู่ให้เกมนั้น

เมื่อเกมปิดลง ระบบจะสุ่มเลือกผู้ชนะและผู้เล่นแต่ละคน มีการปรับค่าสถิติสำหรับ games_played และ games_won อยู่ นอกจากนี้ ผู้เล่นแต่ละคนจะได้รับการอัปเดตเพื่อระบุว่าไม่ได้เล่นแล้วและสามารถเล่นเกมในอนาคตได้

ไฟล์ ./src/golang/matchmaking-service/main.go สำหรับบริการจับคู่จะใช้การตั้งค่าและโค้ดที่คล้ายกับบริการ profile จึงไม่สามารถซ้ำกันที่นี่ บริการนี้แสดงปลายทางหลัก 2 รายการดังนี้

func main() {
   router
:= gin.Default()
   router
.SetTrustedProxies(nil)

   router
.Use(setSpannerConnection())

   router
.POST("/games/create", createGame)
   router
.PUT("/games/close", closeGame)

   router
.Run(configuration.Server.URL())
}

บริการนี้มีโครงสร้างเกม รวมถึงโครงสร้างผู้เล่นและสถิติผู้เล่นที่เล็กลง

type Game struct {
   
GameUUID string           `json:"gameUUID"`
   
Players  []string         `json:"players"`
   
Winner   string           `json:"winner"`
   
Created  time.Time        `json:"created"`
   
Finished spanner.NullTime `json:"finished"`
}

type
Player struct {
   
PlayerUUID   string           `json:"playerUUID"`
   
Stats        spanner.NullJSON `json:"stats"`
   
Current_game string           `json:"current_game"`
}

type
PlayerStats struct {
   
Games_played int `json:"games_played"`
   
Games_won    int `json:"games_won"`
}

ในการสร้างเกม บริการจับคู่จะสุ่มเลือกผู้เล่น 100 คนที่ยังไม่ได้เล่นเกมอยู่

การกลายพันธุ์ของ Spaner จะเลือกมาสร้างเกมและกำหนดผู้เล่น เนื่องจากการกลายพันธุ์จะมีประสิทธิภาพมากกว่า DML เมื่อทำการเปลี่ยนแปลงครั้งใหญ่

// Create a new game and assign players
// Players that are not currently playing a game are eligble to be selected for the new game
// Current implementation allows for less than numPlayers to be placed in a game
func
(g *Game) CreateGame(ctx context.Context, client spanner.Client) error {
   
// Initialize game values
   g
.GameUUID = generateUUID()

   numPlayers
:= 10

   
// Create and assign
   _
, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
       
var m []*spanner.Mutation

       
// get players
       query
:= fmt.Sprintf("SELECT playerUUID FROM (SELECT playerUUID FROM players WHERE current_game IS NULL LIMIT 10000) TABLESAMPLE RESERVOIR (%d ROWS)", numPlayers)
       stmt
:= spanner.Statement{SQL: query}
       iter
:= txn.Query(ctx, stmt)

       playerRows
, err := readRows(iter)
       
if err != nil {
           
return err
       
}

       
var playerUUIDs []string

       
for _, row := range playerRows {
           
var pUUID string
           
if err := row.Columns(&pUUID); err != nil {
               
return err
           
}

           playerUUIDs
= append(playerUUIDs, pUUID)
       
}

       
// Create the game
       gCols
:= []string{"gameUUID", "players", "created"}
       m
= append(m, spanner.Insert("games", gCols, []interface{}{g.GameUUID, playerUUIDs, time.Now()}))

       
// Update players to lock into this game
       
for _, p := range playerUUIDs {
           pCols
:= []string{"playerUUID", "current_game"}
           m
= append(m, spanner.Update("players", pCols, []interface{}{p, g.GameUUID}))
       
}

       txn
.BufferWrite(m)

       
return nil
   
})

   
if err != nil {
       
return err
   
}

   
return nil
}

การเลือกผู้เล่นแบบสุ่มจะทำด้วย SQL โดยใช้ความสามารถ TABLESPACE RESERVOIR ของ GoogleSQL

การปิดเกมจะซับซ้อนขึ้นเล็กน้อย ซึ่งรวมถึงการเลือกผู้ชนะแบบสุ่มจากผู้เล่นเกม การระบุเวลาที่เกมจบ และการอัปเดตของผู้เล่นแต่ละคน สถิติของ games_played และ games_won

เนื่องจากความซับซ้อนและปริมาณการเปลี่ยนแปลงนี้ เราจึงต้องเลือกการเปลี่ยนแปลงอีกครั้งเพื่อปิดเกม

func determineWinner(playerUUIDs []string) string {
   
if len(playerUUIDs) == 0 {
       
return ""
   
}

   
var winnerUUID string

   rand
.Seed(time.Now().UnixNano())
   offset
:= rand.Intn(len(playerUUIDs))
   winnerUUID
= playerUUIDs[offset]
   
return winnerUUID
}

// Given a list of players and a winner's UUID, update players of a game
// Updating players involves closing out the game (current_game = NULL) and
// updating their game stats. Specifically, we are incrementing games_played.
// If the player is the determined winner, then their games_won stat is incremented.
func
(g Game) updateGamePlayers(ctx context.Context, players []Player, txn *spanner.ReadWriteTransaction) error {
   
for _, p := range players {
       
// Modify stats
       
var pStats PlayerStats
       json
.Unmarshal([]byte(p.Stats.String()), &pStats)

       pStats
.Games_played = pStats.Games_played + 1

       
if p.PlayerUUID == g.Winner {
           pStats
.Games_won = pStats.Games_won + 1
       
}
       updatedStats
, _ := json.Marshal(pStats)
       p
.Stats.UnmarshalJSON(updatedStats)

       
// Update player
       
// If player's current game isn't the same as this game, that's an error
       
if p.Current_game != g.GameUUID {
           errorMsg
:= fmt.Sprintf("Player '%s' doesn't belong to game '%s'.", p.PlayerUUID, g.GameUUID)
           
return errors.New(errorMsg)
       
}

       cols
:= []string{"playerUUID", "current_game", "stats"}
       newGame
:= spanner.NullString{
           
StringVal: "",
           
Valid:     false,
       
}

       txn
.BufferWrite([]*spanner.Mutation{
           spanner
.Update("players", cols, []interface{}{p.PlayerUUID, newGame, p.Stats}),
       
})
   
}

   
return nil
}

// Closing game. When provided a Game, choose a random winner and close out the game.
// A game is closed by setting the winner and finished time.
// Additionally all players' game stats are updated, and the current_game is set to null to allow
// them to be chosen for a new game.
func
(g *Game) CloseGame(ctx context.Context, client spanner.Client) error {
   
// Close game
   _
, err := client.ReadWriteTransaction(ctx,
       func
(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
           
// Get game players
           playerUUIDs
, players, err := g.getGamePlayers(ctx, txn)

           
if err != nil {
               
return err
           
}

           
// Might be an issue if there are no players!
           
if len(playerUUIDs) == 0 {
               errorMsg
:= fmt.Sprintf("No players found for game '%s'", g.GameUUID)
               
return errors.New(errorMsg)
           
}

           
// Get random winner
           g
.Winner = determineWinner(playerUUIDs)

           
// Validate game finished time is null
           row
, err := txn.ReadRow(ctx, "games", spanner.Key{g.GameUUID}, []string{"finished"})
           
if err != nil {
               
return err
           
}

           
if err := row.Column(0, &g.Finished); err != nil {
               
return err
           
}

           
// If time is not null, then the game is already marked as finished.
           
// That's an error.
           
if !g.Finished.IsNull() {
               errorMsg
:= fmt.Sprintf("Game '%s' is already finished.", g.GameUUID)
               
return errors.New(errorMsg)
           
}

           cols
:= []string{"gameUUID", "finished", "winner"}
           txn
.BufferWrite([]*spanner.Mutation{
               spanner
.Update("games", cols, []interface{}{g.GameUUID, time.Now(), g.Winner}),
           
})

           
// Update each player to increment stats.games_played
           
// (and stats.games_won if winner), and set current_game
           
// to null so they can be chosen for a new game
           playerErr
:= g.updateGamePlayers(ctx, players, txn)
           
if playerErr != nil {
               
return playerErr
           
}

           
return nil
       
})

   
if err != nil {
       
return err
   
}

   
return nil
}

ระบบจะจัดการการกำหนดค่าอีกครั้งผ่านตัวแปรสภาพแวดล้อมตามที่อธิบายไว้ใน ./src/golang/matchmaking-service/config/config.go สำหรับบริการ

   // Server defaults
   viper
.SetDefault("server.host", "localhost")
   viper
.SetDefault("server.port", 8081)

   
// Bind environment variable override
   viper
.BindEnv("server.host", "SERVICE_HOST")
   viper
.BindEnv("server.port", "SERVICE_PORT")
   viper
.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
   viper
.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
   viper
.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")

เพื่อหลีกเลี่ยงข้อขัดแย้งกับบริการโปรไฟล์ บริการนี้จะทำงานใน localhost:8081 โดยค่าเริ่มต้น

เมื่อมีข้อมูลนี้ ก็ถึงเวลาที่จะเรียกใช้บริการจับคู่แล้ว

เรียกใช้บริการจับคู่

เรียกใช้บริการโดยใช้คำสั่ง go การดำเนินการนี้จะเริ่มต้นให้บริการบนพอร์ต 8082 บริการนี้มีทรัพยากร Dependency เหมือนกับบริการโปรไฟล์หลายรายการ ดังนั้นระบบจะไม่ดาวน์โหลดทรัพยากร Dependency ใหม่

cd ~/spanner-gaming-sample/src/golang/matchmaking-service
go run
. &

เอาต์พุตจากคำสั่ง:

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 
- using env:   export GIN_MODE=release
 
- using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /games/create             --> main.createGame (4 handlers)
[GIN-debug] PUT    /games/close              --> main.closeGame (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8081

สร้างเกม

ทดสอบบริการเพื่อสร้างเกม ก่อนอื่น ให้เปิดเทอร์มินัลใหม่ใน Cloud Shell ดังนี้

90eceac76a6bb90b.png

จากนั้นออกคำสั่ง curl ต่อไปนี้

curl http://localhost:8081/games/create \
   
--include \
   
--header "Content-Type: application/json" \
   
--request "POST"

เอาต์พุตจากคำสั่ง:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 19:38:45 GMT
Content-Length: 38

"f45b0f7f-405b-4e67-a3b8-a624e990285d"

ปิดเกม

curl http://localhost:8081/games/close \
   
--include \
   
--header "Content-Type: application/json" \
   
--data '{"gameUUID": "f45b0f7f-405b-4e67-a3b8-a624e990285d"}' \
   
--request "PUT"

เอาต์พุตจากคำสั่ง:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: <date> 19:43:58 GMT
Content-Length: 38

"506a1ab6-ee5b-4882-9bb1-ef9159a72989"

สรุป

ในขั้นตอนนี้ คุณใช้บริการจับคู่เพื่อจัดการการสร้างเกมและกำหนดให้ผู้เล่นเล่นเกมนั้น บริการนี้ยังจัดการปิดเกม ซึ่งจะสุ่มเลือกผู้ชนะและอัปเดตผู้เล่นเกมทุกคน สถิติของ games_played และ games_won

ขั้นตอนถัดไป

เมื่อบริการของคุณทำงานได้แล้ว ก็ถึงเวลาให้ผู้เล่นลงชื่อสมัครใช้และเล่นเกม

6 เริ่มเล่น

เมื่อโปรไฟล์และบริการจับคู่ทำงานอยู่ คุณสามารถสร้างโหลดโดยใช้โปรแกรมสร้างตัวระบุตำแหน่งที่ให้มา

Locust มีอินเทอร์เฟซเว็บสำหรับเรียกใช้โปรแกรมสร้าง แต่ในห้องทดลองนี้ คุณจะใช้บรรทัดคำสั่ง (ตัวเลือก –headless)

ลงชื่อสมัครใช้ผู้เล่น

ก่อนอื่น คุณจะต้องสร้างผู้เล่น

โค้ด Python สำหรับสร้างโปรแกรมเล่นในไฟล์ ./generators/authentication_server.py จะมีลักษณะดังนี้

class PlayerLoad(HttpUser):
   
def on_start(self):
       
global pUUIDs
       pUUIDs
= []

   
def generatePlayerName(self):
       
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   
def generatePassword(self):
       
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))

   
def generateEmail(self):
       
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32) + ['@'] +
           random
.choices(['gmail', 'yahoo', 'microsoft']) + ['.com'])

   
@task
   
def createPlayer(self):
       headers
= {"Content-Type": "application/json"}
       data
= {"player_name": self.generatePlayerName(), "email": self.generateEmail(), "password": self.generatePassword()}

       
with self.client.post("/players", data=json.dumps(data), headers=headers, catch_response=True) as response:
           
try:
               pUUIDs
.append(response.json())
           
except json.JSONDecodeError:
               response
.failure("Response could not be decoded as JSON")
           
except KeyError:
               response
.failure("Response did not contain expected key 'gameUUID'")

ชื่อผู้เล่น อีเมล และรหัสผ่านจะสร้างขึ้นแบบสุ่ม

ระบบจะดึงข้อมูลผู้เล่นที่ลงชื่อสมัครใช้สำเร็จแล้วในงานที่ 2 เพื่อสร้างภาระงานการอ่าน

   @task(5)
   
def getPlayer(self):
       
# No player UUIDs are in memory, reschedule task to run again later.
       
if len(pUUIDs) == 0:
           
raise RescheduleTask()

       
# Get first player in our list, removing it to avoid contention from concurrent requests
       pUUID
= pUUIDs[0]
       
del pUUIDs[0]

       headers
= {"Content-Type": "application/json"}

       
self.client.get(f"/players/{pUUID}", headers=headers, name="/players/[playerUUID]")

คำสั่งต่อไปนี้เรียกไฟล์ ./generators/authentication_server.py ที่จะสร้างโปรแกรมเล่นใหม่เป็นเวลา 30 วินาที (t=30s) โดยมีการเกิดขึ้นพร้อมกัน 2 ชุดข้อความพร้อมกัน (u=2) ดังนี้

cd ~/spanner-gaming-sample
locust -H http:/
/127.0.0.1:8080 -f ./generators/authentication_server.py --headless -u=2 -r=2 -t=30s

ผู้เล่นเข้าร่วมเกม

เมื่อคุณมีผู้เล่นลงชื่อสมัครใช้แล้ว พวกเขาต้องการเริ่มเล่นเกม!

โค้ด Python ที่ใช้สร้างและปิดเกมในไฟล์ ./generators/match_server.py จะมีลักษณะดังนี้

from locust import HttpUser, task
from locust.exception import RescheduleTask

import json

class GameMatch(HttpUser):
   
def on_start(self):
       
global openGames
       openGames
= []

   
@task(2)
   
def createGame(self):
       headers
= {"Content-Type": "application/json"}

       
# Create the game, then store the response in memory of list of open games.
       
with self.client.post("/games/create", headers=headers, catch_response=True) as response:
           
try:
               openGames
.append({"gameUUID": response.json()})
           
except json.JSONDecodeError:
               response
.failure("Response could not be decoded as JSON")
           
except KeyError:
               response
.failure("Response did not contain expected key 'gameUUID'")


   
@task
   
def closeGame(self):
       
# No open games are in memory, reschedule task to run again later.
       
if len(openGames) == 0:
           
raise RescheduleTask()

       headers
= {"Content-Type": "application/json"}

       
# Close the first open game in our list, removing it to avoid
       
# contention from concurrent requests
       game
= openGames[0]
       
del openGames[0]

       data
= {"gameUUID": game["gameUUID"]}
       
self.client.put("/games/close", data=json.dumps(data), headers=headers)

เมื่อเรียกใช้โปรแกรมสร้างนี้ โปรแกรมจะเปิดและปิดเกมที่อัตราส่วน 2:1 (open:close) คำสั่งนี้จะเรียกใช้โปรแกรมสร้างเป็นเวลา 10 วินาที (-t=10s):

locust -H http://127.0.0.1:8081 -f ./generators/match_server.py --headless -u=1 -r=1 -t=10s

สรุป

ในขั้นตอนนี้ คุณได้จำลองให้ผู้เล่นลงชื่อสมัครใช้เกม แล้วทำการจำลองให้ผู้เล่นเล่นเกมโดยใช้บริการจับคู่ การจำลองเหล่านี้ใช้ประโยชน์จากเฟรมเวิร์ก Locust Python เพื่อส่งคำขอไปยังบริการของเรา REST API

แก้ไขเวลาที่ใช้ในการสร้างผู้เล่นและเล่นเกม รวมถึงจำนวนผู้ใช้ที่ใช้งานพร้อมกัน (-u)

ขั้นตอนถัดไป

หลังจากการจำลอง คุณจะต้องตรวจสอบสถิติต่างๆ โดยค้นหา Spanner

7 ดึงข้อมูลสถิติเกม

เมื่อเรามีผู้เล่นจำลองสามารถลงชื่อสมัครใช้และเล่นเกมแล้ว คุณควรตรวจสอบสถิติของคุณ

ซึ่งทำได้โดยใช้ Cloud Console ในการส่งคำขอการค้นหาไปยัง Spanner

b5e3154c6f7cb0cf.png

การตรวจสอบการแข่งขันแบบเปิดเทียบกับแบบปิด

เกมที่ปิดไปแล้วคือเกมที่มีการประทับเวลาจบแล้ว ขณะที่เกมที่เปิดอยู่จะจบแล้วแสดงเป็น NULL ค่านี้จะตั้งเมื่อเกมปิดไปแล้ว

ดังนั้นการค้นหานี้จะช่วยคุณตรวจสอบจำนวนเกมที่เปิดอยู่และจำนวนเกมที่ปิดไปแล้ว

SELECT Type, NumGames FROM
(SELECT "Open Games" as Type, count(*) as NumGames FROM games WHERE finished IS NULL
UNION ALL
SELECT
"Closed Games" as Type, count(*) as NumGames FROM games WHERE finished IS NOT NULL
)

ผลลัพธ์

Type

NumGames

Open Games

0

Closed Games

175

การตรวจสอบจำนวนผู้เล่นที่เล่นและไม่เล่น

ผู้เล่นกำลังเล่นเกมหากมีการตั้งค่าคอลัมน์ current_game ไว้ ไม่เช่นนั้น ผู้ใช้จะไม่เล่นเกมอยู่

หากต้องการเปรียบเทียบจำนวนผู้เล่นที่กำลังเล่นและไม่ได้เล่นอยู่ ให้ใช้คำค้นหานี้

SELECT Type, NumPlayers FROM
(SELECT "Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NOT NULL
UNION ALL
SELECT
"Not Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NULL
)

ผลลัพธ์

Type

NumPlayers

Playing

0

Not Playing

310

กำหนดผู้ชนะสูงสุด

เมื่อเกมปิดลง ระบบจะสุ่มเลือกผู้เล่นคนใดคนหนึ่งให้เป็นผู้ชนะ สถิติ games_won ของผู้เล่นรายนั้นจะเพิ่มขึ้นในช่วงปิดเกม

SELECT playerUUID, stats
FROM players
WHERE CAST
(JSON_VALUE(stats, "$.games_won") AS INT64)>0
LIMIT
10;

ผลลัพธ์

playerUUID

สถิติ

07e247c5-f88e-4bca-a7bc-12d2485f2f2b

{&quot;games_played&quot;:49,&quot;games_won&quot;:1}

09b72595-40af-4406-a000-2fb56c58fe92

{&quot;games_played&quot;:56,&quot;games_won&quot;:1}

1002385b-02a0-462b-a8e7-05c9b27223aa

{&quot;games_played&quot;:66,&quot;games_won&quot;:1}

13ec3770-7ae3-495f-9b53-6322d8e8d6c3

{&quot;games_played&quot;:44,&quot;games_won&quot;:1}

15513852-3f2a-494f-b437-fe7125d15f1b

{&quot;games_played&quot;:49,&quot;games_won&quot;:1}

17faec64-4f77-475c-8df8-6ab026cf6698

{&quot;games_played&quot;:50,&quot;games_won&quot;:1}

1abfcb27-037d-446d-bb7a-b5cd17b5733d

{&quot;games_played&quot;:63,&quot;games_won&quot;:1}

2109a33e-88bd-4e74-a35c-a7914d9e3bde

{&quot;games_played&quot;:56,&quot;games_won&quot;:2}

222e37d9-06b0-4674-865d-a0e5fb80121e

{&quot;games_played&quot;:60,&quot;games_won&quot;:1}

22ced15c-0da6-4fd9-8cb2-1ffd233b3c56

{&quot;games_played&quot;:50,&quot;games_won&quot;:1}

สรุป

ในขั้นตอนนี้ คุณได้ตรวจสอบสถิติต่างๆ ของผู้เล่นและเกมโดยใช้ Cloud Console เพื่อค้นหา Spanner

ขั้นตอนถัดไป

ต่อไปก็ถึงเวลาล้างข้อมูล

8 กำลังล้างข้อมูล (ไม่บังคับ)

หากต้องการล้างข้อมูล เพียงไปที่ส่วน Cloud Spanner ของ Cloud Console แล้วลบอินสแตนซ์ 'cloudspanner-gaming' ที่เราสร้างในขั้นตอน Codelab ที่ชื่อ "Setup a Cloud Spannerอินสแตนซ์"

9 ยินดีด้วย

ขอแสดงความยินดี คุณทำให้เกมตัวอย่างใช้งานได้ใน Spanner เรียบร้อยแล้ว

ขั้นตอนถัดไปคือ

ในห้องทดลองนี้ คุณได้รู้จักกับหัวข้อต่างๆ เกี่ยวกับการทำงานร่วมกับ Spanner โดยใช้ไดรเวอร์โกลัง ซึ่งจะให้พื้นฐานที่ดียิ่งขึ้นเพื่อทำความเข้าใจแนวคิดสำคัญ เช่น

  • การออกแบบสคีมา
  • DML เทียบกับการเปลี่ยนแปลง
  • การทำงานร่วมกับ Golang

อย่าลืมดู Codelab ของ Cloud Spanner Game Trading Post เพื่อใช้เป็นตัวอย่างสำหรับการทำงานกับ Spanner เป็นแบ็กเอนด์สำหรับเกมของคุณ