Itβs hard to find the right information on Russian-language resources, perhaps this material will allow you to understand some of the basics for creating multi-player games and more. I plan to make a series of articles on creating 2.5D MMORPG, that is, in isometry, our world will be divided into procedurally generated chunks consisting of titles. The server will be written in the Golang language, which seems to me perfectly suitable for this, the client part will be in JavaScript using the framework - Phaser.jsCreate a world generation
And so in this article we will write a chunk generator for MMO on the Golang, we will not consider Phaser for now. For procedural generation, we need a noise function, we will use Perlin Noise , I recommend that you read this article and rewrite the code for Go or take my version.Perlin.gopackage PerlinNoise
import (
"math"
"math/rand"
)
func Noise(x, y float32) float32 {
left := float32(math.Floor(float64(x)))
top := float32(math.Floor(float64(y)))
localPoinX := x - left
localPoiny := y - top
topLeft := getRandomVector(left, top)
topRight := getRandomVector(left+1, top)
bottomLeft := getRandomVector(left, top+1)
bottomRight := getRandomVector(left+1, top+1)
DtopLeft := []float32{localPoinX, localPoiny}
DtopRight := []float32{localPoinX - 1, localPoiny}
DbottomLeft := []float32{localPoinX, localPoiny - 1}
DbottomRight := []float32{localPoinX - 1, localPoiny - 1}
tx1 := dot(DtopLeft, topLeft)
tx2 := dot(DtopRight, topRight)
bx1 := dot(DbottomLeft, bottomLeft)
bx2 := dot(DbottomRight, bottomRight)
pointX := curve(localPoinX)
pointY := curve(localPoiny)
tx := lerp(tx1, tx2, pointX)
bx := lerp(bx1, bx2, pointX)
tb := lerp(tx, bx, pointY)
return tb
}
func getRandomVector(x, y float32) []float32 {
rand.Seed(int64(x * y))
v := rand.Intn(3)
switch v {
case 0:
return []float32{-1, 0}
case 1:
return []float32{1, 0}
case 2:
return []float32{0, 1}
default:
return []float32{0, -1}
}
}
func dot(a []float32, b []float32) float32 {
return (a[0]*b[0] + b[1]*a[1])
}
func lerp(a, b, c float32) float32 {
return a*(1-c) + b*c
}
func curve(t float32) float32 {
return (t * t * t * (t*(t*6-15) + 10))
}
Let's create a small project where we test the functionality of our function, here is the structure of my project:
Add the following to the main.go file:func main() {
var a, b float32= 1330, 2500
v:= PerlinNoise.Noise(a/2500, b/2500)
fmt.Println(v)
}
Be careful with the types of numbers, always specify the types explicitly, this will save you from problems in the future, the output of the function:-0.23416707
And so we have a noise function to generate our worlds. Let's get started creating chunks. Create the Chunk directory and the Chunk.go file in it and immediately define the main constants:var TILE_SIZE = 16
var CHUNK_SIZE = TILE_SIZE * TILE_SIZE
var PERLIN_SEED float32 = 150
TILE_SIZE is the resolution of our future chunks in pixelsCHUNK_SIZE is the size of the chunk, in this case 16 * 16PERLIN_SEED - here you can put any number, the higher it is, the more uniform the noise, i.e. if you want small islands, then put the number less, if the huge continents are higher.Next, create a data type for the coordinates:type Coordinate struct {
X int
Y int
}
This type will be very useful to us in the future, and now we will create another important function, to determine the unique coordinates of our chunk in the future we will call their ID:func GetChunkID(x, y int) Coordinate {
tileX := float64(float64(x) / float64(TILE_SIZE))
tileY := float64(float64(y) / float64(TILE_SIZE))
var ChunkID Coordinate
if tileX < 0 {
ChunkID.X = int(math.Floor(tileX / float64(TILE_SIZE)))
} else {
ChunkID.X = int(math.Ceil(tileX / float64(TILE_SIZE)))
}
if tileY < 0 {
ChunkID.Y = int(math.Floor(tileY / float64(TILE_SIZE)))
} else {
ChunkID.Y = int(math.Ceil(tileY / float64(TILE_SIZE)))
}
if tileX == 0 {
ChunkID.X = 1
}
if tileY == 0 {
ChunkID.Y = 1
}
return ChunkID
}
The function for determining the ID of the chunk is quite simple, we just divide the position on the map by the size of the tile, and then we divide the result again by the size of the tile with rounding up or down, depending on the ID of the chunk since our world will be generated endlessly in any direction.Next, add our building block to create the chunk, this is the tile and the chunk itself:type Chunk struct {
ChunkID [2]int
Map map[Coordinate]Tile
}
type Tile struct {
Key string
X int
Y int
}
The chunk contains a tile map. Tiles store their coordinates and key (the key is the type of your title: land, water, mountains, etc.)Now let's move on to the most important, the functions for creating our chunk, I took my working function from the project and redid it a bit for this article:func NewChunk ()func NewChunk(idChunk Coordinate) Chunk {
chunk := Chunk{ChunkID: [2]int{idChunk.X, idChunk.Y}}
var chunkXMax, chunkYMax int
var chunkMap map[Coordinate]Tile
chunkMap = make(map[Coordinate]Tile)
chunkXMax = idChunk.X * CHUNK_SIZE
chunkYMax = idChunk.Y * CHUNK_SIZE
switch {
case chunkXMax < 0 && chunkYMax < 0:
{
for x := chunkXMax + CHUNK_SIZE; x > chunkXMax; x -= TILE_SIZE {
for y := chunkYMax + CHUNK_SIZE; y > chunkYMax; y -= TILE_SIZE {
posX := float32(x - (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.01:
tile.Key = "~"
case perlinValue >= -0.01 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
case chunkXMax < 0:
{
for x := chunkXMax + CHUNK_SIZE; x > chunkXMax; x -= TILE_SIZE {
for y := chunkYMax - CHUNK_SIZE; y < chunkYMax; y += TILE_SIZE {
posX := float32(x - (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
case chunkYMax < 0:
{
for x := chunkXMax - CHUNK_SIZE; x < chunkXMax; x += TILE_SIZE {
for y := chunkYMax + CHUNK_SIZE; y > chunkYMax; y -= TILE_SIZE {
posX := float32(x + (TILE_SIZE / 2))
posY := float32(y - (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
default:
{
for x := chunkXMax - CHUNK_SIZE; x < chunkXMax; x += TILE_SIZE {
for y := chunkYMax - CHUNK_SIZE; y < chunkYMax; y += TILE_SIZE {
posX := float32(x + (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
}
chunk.Map = chunkMap
return chunk
}
And so, in this function or rather the constructor of our chunk, we determine the maximum coordinates of the chunk, to which we will move sequentially, filling the tiles with the necessary information. ChunkMax is also determined quite simply, for this we multiply the chunk ID by its size (CHUNK_SIZE), that is, with ID {1; 1} our coordinates chunkXMax and chunkYMax will be 256.In posX / posY we determine the coordinates for inserting our graphics: posX := float32(x + (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
We use switch to select the logic depending on the value of the ID of our chunk (There may be positive and negative values). The key of the tile will determine the noise of perlin, for example, if the noise of perlin is below 0 it will be water, above it will be land. We will do this: case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
Let's see how our function works, replace the code in main with the following contents:func main() {
coord := Chunk.Coordinate{Y: 1, X: 1}
chunk := Chunk.NewChunk(coord)
m := chunk.Map
out := os.Stdout
for y := 8; y < 16*16; y += 16 {
for x := 8; x < 16*16; x += 16 {
c := Chunk.Coordinate{X: x, Y: y}
out.Write([]byte(m[c].Key))
}
out.Write([]byte("\n"))
}
}
Conclusion:11~~~11111111111
11~~~11111111111
11~~~~1111111111
11~~~~1111111111
11~~~~~111111111
11~~~~~~1111111~
11~~~~~~~~~11~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~~~~~
11~~~~~~~~~~1111
11~~~~~~~~~11111
It looks good, you can change the output function and play with the parameters:11111~~~~~~~~111111111111111111111111111111~~~~~~~11111111111111
11111~~~~~~~~111111111111111111111111111111~~~~~~~11111111111111
11111~~~~~~~~111111111111111111111111111111~~~~~~~11111111111111
11111~~~~~~~~111111111111111111111111111111~~~~~~~~1111111111111
11111~~~~~~~~11111111111111111111111111111~~~~~~~~~1111111111111
11111~~~~~~~~11111111111111111111111111111~~~~~~~~~1111111111111
11111~~~~~~~~~111111111111111111111111111~~~~~~~~~~~111111111111
11111~~~~~~~~~111111111111111111111111111~~~~~~~~~~~~11111111111
11111~~~~~~~~~~1111111111111111111111111~~~~~~~~~~~~~11111111111
11111~~~~~~~~~~~11111111111111111111111~~~~~~~~~~~~~~~1111111111
11111~~~~~~~~~~~1111111111111111111111~~~~~~~~~~~~~~~~~111111111
11111~~~~~~~~~~~~~11111111111111111111~~~~~~~~~~~~~~~~~111111111
11111~~~~~~~~~~~~~~111111111111111111~~~~~~~~~~~~~~~~~~~11111111
11111~~~~~~~~~~~~~~~~11111111111111~~~~~~~~~~~~~~~~~~~~~~1111111
11111~~~~~~~~~~~~~~~~~~11111111111~~~~~~~~~~~~~~~~~~~~~~~~111111
11111~~~~~~~~~~~~~~~~~~~~~111111~~~~~~~~~~~~~~~~~~~~~~~~~~~11111
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1111
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~111
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~11
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~1
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~~~1111111111~~~~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~~~111111111111111~~~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~~1111111111111111111~~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~~111111111111111111111~~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~~11111111111111111111111~~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~~1111111111111111111111111~~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~~111111111111111111111111111~~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~11111111111111111111111111111~~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~~111111111111111111111111111111~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~~1111111111111111111111111111111~~~~~~~~~~~
11111~~~~~~~~~~~~~~~~111111111111111111111111111111111~~~~~~~~~~
11111~~~~~~~~~~~~~~~~1111111111111111111111111111111111~~~~~~~~~
11111~~~~~~~~~~~~~~~~11111111111111111111111111111111111~~~~~~~~
11111~~~~~~~~~~~~~~~~11111111111111111111111111111111111~~~~~~~~
11111~~~~~~~~~~~~~~~~111111111111111111111111111111111111~~~~~~~
11111~~~~~~~~~~~~~~~~1111111111111111111111111111111111111~~~~~~
11111~~~~~~~~~~~~~~~~1111111111111111111111111111111111111~~~~~~
11111~~~~~~~~~~~~~~~~11111111111111111111111111111111111111~~~~~
11111~~~~~~~~~~~~~~~~11111111111111111111111111111111111111~~~~~
11111~~~~~~~~~~~~~~~~~11111111111111111111111111111111111111~~~~
11111~~~~~~~~~~~~~~~~~11111111111111111111111111111111111111~~~~
11111~~~~~~~~~~~~~~~~~~11111111111111111111111111111111111111~~~
1111~~~~~~~~~~~~~~~~~~~11111111111111111111111111111111111111~~~
1111~~~~~~~~~~~~~~~~~~~~1111111111111111111111111111111111111~~~
1111~~~~~~~~~~~~~~~~~~~~~111111111111111111111111111111111111~~~
1111~~~~~~~~~~~~~~~~~~~~~1111111111111111111111111111111111111~~
1111~~~~~~~~~~~~~~~~~~~~~~111111111111111111111111111111111111~~
1111~~~~~~~~~~~~~~~~~~~~~~~11111111111111111111111111111111111~~
1111~~~~~~~~~~~~~~~~~~~~~~~~1111111111111111111111111111111111~~
1111~~~~~~~~~~~~~~~~~~~~~~~~1111111111111111111111111111111111~~
1111~~~~~~~~~~~~~~~~~~~~~~~~~111111111111111111111111111111111~~
The code:Chunk.gopackage Chunk
import (
"PerlinNoise"
"math"
)
var TILE_SIZE = 16
var CHUNK_SIZE = 32 * 32
var PERLIN_SEED float32 = 600
type Coordinate struct {
X int `json:"x"`
Y int `json:"y"`
}
type Chunk struct {
ChunkID [2]int
Map map[Coordinate]Tile
}
type Tile struct {
Key string
X int
Y int
}
func GetChunkID(x, y int) Coordinate {
tileX := float64(x)
tileY := float64(y)
var ChunkID Coordinate
if tileX < 0 {
ChunkID.X = int(math.Floor(tileX / float64(TILE_SIZE)))
} else {
ChunkID.X = int(math.Ceil(tileX / float64(TILE_SIZE)))
}
if tileY < 0 {
ChunkID.Y = int(math.Floor(tileY / float64(TILE_SIZE)))
} else {
ChunkID.Y = int(math.Ceil(tileY / float64(TILE_SIZE)))
}
if tileX == 0 {
ChunkID.X = 1
}
if tileY == 0 {
ChunkID.Y = 1
}
return ChunkID
}
func NewChunk(idChunk Coordinate) Chunk {
chunk := Chunk{ChunkID: [2]int{idChunk.X, idChunk.Y}}
var chunkXMax, chunkYMax int
var chunkMap map[Coordinate]Tile
chunkMap = make(map[Coordinate]Tile)
chunkXMax = idChunk.X * CHUNK_SIZE
chunkYMax = idChunk.Y * CHUNK_SIZE
switch {
case chunkXMax < 0 && chunkYMax < 0:
{
for x := chunkXMax + CHUNK_SIZE; x > chunkXMax; x -= TILE_SIZE {
for y := chunkYMax + CHUNK_SIZE; y > chunkYMax; y -= TILE_SIZE {
posX := float32(x - (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.01:
tile.Key = "~"
case perlinValue >= -0.01 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
case chunkXMax < 0:
{
for x := chunkXMax + CHUNK_SIZE; x > chunkXMax; x -= TILE_SIZE {
for y := chunkYMax - CHUNK_SIZE; y < chunkYMax; y += TILE_SIZE {
posX := float32(x - (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
case chunkYMax < 0:
{
for x := chunkXMax - CHUNK_SIZE; x < chunkXMax; x += TILE_SIZE {
for y := chunkYMax + CHUNK_SIZE; y > chunkYMax; y -= TILE_SIZE {
posX := float32(x + (TILE_SIZE / 2))
posY := float32(y - (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
default:
{
for x := chunkXMax - CHUNK_SIZE; x < chunkXMax; x += TILE_SIZE {
for y := chunkYMax - CHUNK_SIZE; y < chunkYMax; y += TILE_SIZE {
posX := float32(x + (TILE_SIZE / 2))
posY := float32(y + (TILE_SIZE / 2))
tile := Tile{}
tile.X = int(posX)
tile.Y = int(posY)
perlinValue := PerlinNoise.Noise(posX/PERLIN_SEED, posY/PERLIN_SEED)
switch {
case perlinValue < -0.12:
tile.Key = "~"
case perlinValue >= -0.12 && perlinValue <= 0.5:
tile.Key = "1"
case perlinValue > 0.5:
tile.Key = "^"
}
chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile
}
}
}
}
chunk.Map = chunkMap
return chunk
}
Perlin.gopackage PerlinNoise
import (
"math"
"math/rand"
)
func Noise(x, y float32) float32 {
left := float32(math.Floor(float64(x)))
top := float32(math.Floor(float64(y)))
localPoinX := x - left
localPoiny := y - top
topLeft := getRandomVector(left, top)
topRight := getRandomVector(left+1, top)
bottomLeft := getRandomVector(left, top+1)
bottomRight := getRandomVector(left+1, top+1)
DtopLeft := []float32{localPoinX, localPoiny}
DtopRight := []float32{localPoinX - 1, localPoiny}
DbottomLeft := []float32{localPoinX, localPoiny - 1}
DbottomRight := []float32{localPoinX - 1, localPoiny - 1}
tx1 := dot(DtopLeft, topLeft)
tx2 := dot(DtopRight, topRight)
bx1 := dot(DbottomLeft, bottomLeft)
bx2 := dot(DbottomRight, bottomRight)
pointX := curve(localPoinX)
pointY := curve(localPoiny)
tx := lerp(tx1, tx2, pointX)
bx := lerp(bx1, bx2, pointX)
tb := lerp(tx, bx, pointY)
return tb
}
func getRandomVector(x, y float32) []float32 {
rand.Seed(int64(x * y))
v := rand.Intn(3)
switch v {
case 0:
return []float32{-1, 0}
case 1:
return []float32{1, 0}
case 2:
return []float32{0, 1}
default:
return []float32{0, -1}
}
}
func dot(a []float32, b []float32) float32 {
return (a[0]*b[0] + b[1]*a[1])
}
func lerp(a, b, c float32) float32 {
return a*(1-c) + b*c
}
func curve(t float32) float32 {
return (t * t * t * (t*(t*6-15) + 10))
}
main.gopackage main
import (
"fmt"
"habr/Chunk"
"os"
)
func main() {
coord:= Chunk.GetChunkID(0,0)
fmt.Println(coord)
chunk := Chunk.NewChunk(coord)
m := chunk.Map
out := os.Stdout
for y := 8; y < 32*32; y += 16 {
for x := 8; x < 32*32; x += 16 {
c := Chunk.Coordinate{X: x, Y: y}
out.Write([]byte(m[c].Key))
}
out.Write([]byte("\n"))
}
}
In the next article , we will consider working with HTTP, and if we affect it, then the WS connection. Let's create a type of game card, which we will serialize in json format for rendering on the client and generally see how we interact with the client.