javascript

Golang + Phaser3 = MMORPG — Клиент и Сервер

  • четверг, 20 февраля 2020 г. в 00:21:15
https://habr.com/ru/post/488794/
  • JavaScript
  • Go


image

В прошлой статье мы сделали с вами заготовку, так сказать основу, на чем будет создаваться наша вселенная, визуализация с помощью консоли может быть и выглядит хорошо, но текстовые символы это скучно и не очень красиво, в этой статье мы займемся визуализацией наших тайлов с помощью Phaser.js

В прошлой статье наш проект выглядил так:

image

Теперь мы будем использовать и другие инструменты для веб-разработки, надеюсь у вас установлен Node.js и npm, если нет, то срочно установите. И так открываем терминал и запускам:

$ npm install phaser@3.22.0

При удачном завершении команды мы должны увидеть следующее:

+ phaser@3.22.0
added 15 packages from 48 contributors and audited 20 packages in 4.38s

image

Так отлично, появились модули, теперь мы создадим директорию для нашего клиента

image

В Content мы будет хранить ресурсы игры, т.е. наши спрайты. Так же создадим два файла game.js и MainScene.js, в корневом каталоге(где лежит файл main.go) создадим index.html
game.js — хранит основные настройки для игры
MainScene.js — будет содержать класс основной сцены игры
index.html — страница где будет происходить рендер сцены

Сразу подключим в index.html наши скрипты и больше мы к данному файлу возвращаться не будем:

    <script src="node_modules/phaser/dist/phaser.js" type="module"></script>
    <script src="Client/game.js" type="module"></script>

В MainScene.js сделаем небольшой шаблон класса нашей будущей сцены:

export {MainScene}
class MainScene extends Phaser.Scene{
constructor() {
    super({key: 'MainScene'})
}
preload() {

}
create() {

}
update() {

}
}

В game.js добавьте типовые настройки по своему вкусу, вот мои:

import {MainScene} from "./MainScene.js";
let config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    disableContextMenu: true,
    background: 'black',
    physics: {
        default: 'arcade',
        arcadePhysics: {
            overlapBias: 1
        }
    },
    scene:[MainScene],
    pixelArt: true,
    roundPixels: true,
    antialias: true

}
let game = new Phaser.Game(config);

Теперь нам нужен HTTP сервер, на го это делается в несколько строк. Переходим в main.go и создадим сервер:

package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func main() {
	// Роутер для доступа к клиенту
	http.HandleFunc("/", indexHandler)
	// Открываем доступ к статичным ресурсам (скрипты, картинки и тд.)
	http.Handle("/node_modules/phaser/dist/", http.StripPrefix("/node_modules/phaser/dist/", http.FileServer(http.Dir("./node_modules/phaser/dist/"))))
	http.Handle("/Client/", http.StripPrefix("/Client/", http.FileServer(http.Dir("./Client/"))))
	http.Handle("/Client/Content/", http.StripPrefix("/Client/Content/", http.FileServer(http.Dir("./Client/Content/"))))
	// Запускаем сервер. Указываем любой порт, я выбрал 8080
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		fmt.Println(err.Error())
	}
}
// Обработчик для index.html, здесь мы просто отдаем клиент пользователю
func indexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("indexAction")
	t, _ := template.ParseFiles("index.html")
	err := t.Execute(w, "index")
	if err != nil {
		fmt.Println(err.Error())
	}
}

Ну вот, у нас есть собственный веб-сервер и клиент! Давайте уже запустим! Открываем консоль:

$ go run main.go

Открываем браузер и попробуем подключиться к нашему серверу, в моем случае это
localhost:8080

image

Если вы увидели черный экран, значит вы все сделали правильно.

И так, давайте создадим еще один обработчик, по которому мы будем получать наш чанк в json формате. Создадим отдельную директорию и назовем ее GameController, здесь у нас будут все обработчики работающие с игровыми данными, создадим файл Map_Controller.go

Так же нам понадобится улучшенный

Chunk.go
package Chunk

import (
	"exampleMMO/PerlinNoise"
	"fmt"
)


var TILE_SIZE = 16
var CHUNK_SIZE = 16 * 16
var PERLIN_SEED float32 = 160

type Chunk struct {
	ChunkID [2]int `json:"chunkID"`
	Map     map[Coordinate]Tile `json:"map"`
}

/*
Тайтл игрового мира
*/
type Tile struct {
	Key string `json:"key"`
	X   int    `json:"x"`
	Y   int    `json:"y"`
}

/*
Универсальная структура для хранения координат
*/
type Coordinate struct {
	X int `json:"x"`
	Y int `json:"y"`
}



/*
Создает карту чанка из тайлов, генерирует карту на основе координаты чанка
Например [1,1]
*/
func NewChunk(idChunk Coordinate) Chunk {
	fmt.Println("New Chank", idChunk)
	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 = "Water"
					case perlinValue >= -0.01 && perlinValue < 0:
						tile.Key = "Sand"
					case perlinValue >= 0 && perlinValue <= 0.5:
						tile.Key = "Ground"
					case perlinValue > 0.5:
						tile.Key = "Mount"
					}
					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 = "Water"
					case perlinValue >= -0.12 && perlinValue <= 0.5:
						tile.Key = "Ground"
					case perlinValue > 0.5:
						tile.Key = "Mount"
					}
					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 = "Water"
					case perlinValue >= -0.12 && perlinValue <= 0.5:
						tile.Key = "Ground"
					case perlinValue > 0.5:
						tile.Key = "Mount"
					}
					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 = "Water"
					case perlinValue >= -0.12 && perlinValue <= 0.5:
						tile.Key = "Ground"
					case perlinValue > 0.5:
						tile.Key = "Mount"
					}
					chunkMap[Coordinate{X: tile.X, Y: tile.Y}] = tile

				}
			}
		}

	}
	chunk.Map = chunkMap
	return chunk
}


Мы просто добавили json ключи к нашим структурам и немного улучшили создание чанка
Возвращаемся к Map_Controller,

package GameController

import (
	"encoding/json"
	"exampleMMO/Chunk"
	"fmt"
	"net/http"
)

func Map_Handler(w http.ResponseWriter, r *http.Request) {
			c:= Chunk.NewChunk(Chunk.Coordinate{1,1})
			 js, e :=json.Marshal(c)
			 if e!= nil {
			 	fmt.Println(e.Error())
			 }
			 fmt.Println(string(js))
}

и добавьте строчку в main.go

	http.HandleFunc("/map", GameController.Map_Handler)

Попробуем запустить сервер и перейти по адресу localhost:8080/map

Вывод в терминале:

New Chank {1 1}
json: unsupported type: map[Chunk.Coordinate]Chunk.Tile

Да, мы забыли что в Golang при сериализации, ключи карты должны быть строкой. Для сериализации Go проверяет соответствует ли тип интерфейсу TextMarshaler, и вызывает его метод MarshalText(), нам нужно просто создать метод MarshalText() для нашего типа Coordinate
Возвращаемся в Chunk.go и добавляет следующий код:

func (t Coordinate) MarshalText() ([]byte, error) {

	return []byte("[" + strconv.Itoa(t.X) + "," + strconv.Itoa(t.Y) + "]"), nil
}

Вы можете написать свою реализацию, самое главное, чтобы данный метод возвращал уникальную строку. Данный ключ мы будем использовать для управления чанками на клиенте, давайте теперь проверим как работает наш контроллер, запустим еще раз сервер и посмотрим вывод в консоль

image

Да, все отлично, давайте теперь сделаем вывод в поток, добавим в конце нашего контроллера две строчки:


	w.Header().Set("Content-Type", "application/json")
	w.Write(js)

Пока закончим с Golang и вернемся к клиенту. нам потребуются три тайтла, хотя по факту у нас их 4, но нам пока что хватит трех, а то и двух.







Добавим наши тайлы в директорию Content и начнем работать с MainScene.js, для первых результатов нам хватит нескольких функций:

class MainScene extends Phaser.Scene{
constructor() {
    super({key: 'MainScene'})

}
preload() {
    // Загружаем наши ресурсы в игру
    this.load.image("Ground", "Client/Content/sprGrass.png")
    this.load.image("Water", "Client/Content/sprWater1.png")
    this.load.image("Sand", "Client/Content/sprGrass.png")


}
create() {
    this.getGameMap()
}
update() {

}
// Получаем карту чанка
async getGameMap() {
    let res = await fetch("/map")
    let result = await res.json()    
    this.drawChunk(result.map)

}
// Рисуем наши тайлы на игровом поле
drawChunk(map) {
    for (let chunkKey in map) {
        this.add.image(map[chunkKey].x,map[chunkKey].y, map[chunkKey].key)
    }
}

}

Сервер возвращает нам наш чанк в виде json объекта, вы можете посмотреть в консоли браузера его структуру:

image

А вот так Phaser его отрисовал в браузере:

image

Мы рассмотрели простейшую работу между сервером и клиентом, в следующей статье мы уже будем сразу отрисовывать 9 чанков и перемещаться по миру. Весь код и ресурсы из статьи смотрите здесь.