В прошлой статье мы сделали с вами заготовку, так сказать основу, на чем будет создаваться наша вселенная, визуализация с помощью консоли может быть и выглядит хорошо, но текстовые символы это скучно и не очень красиво, в этой статье мы займемся визуализацией наших тайлов с помощью Phaser.js
В прошлой статье наш проект выглядел так:
Теперь мы будем использовать и другие инструменты для веб-разработки, надеюсь у вас установлен 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
Так отлично, появились модули, теперь мы создадим директорию для нашего клиента
В 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
Если вы увидели черный экран, значит вы все сделали правильно.
И так, давайте создадим еще один обработчик, по которому мы будем получать наш чанк в 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
}
Вы можете написать свою реализацию, самое главное, чтобы данный метод возвращал уникальную строку. Данный ключ мы будем использовать для управления чанками на клиенте, давайте теперь проверим как работает наш контроллер, запустим еще раз сервер и посмотрим вывод в консоль
Да, все отлично, давайте теперь сделаем вывод в поток, добавим в конце нашего контроллера две строчки:
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 объекта, вы можете посмотреть в консоли браузера его структуру:
А вот так Phaser его отрисовал в браузере:
Мы рассмотрели простейшую работу между сервером и клиентом, в следующей статье мы уже будем сразу отрисовывать 9 чанков и перемещаться по миру. Весь код и ресурсы из статьи смотрите здесь.
Shtucer
Ой-ой… где-то заплакал гошник
Lolodin1488 Автор
Как бы гошник назвал такой пакет, у меня если честно нет опыта в программировании и просто пишу мысли, как бы сделал я.