Golang + Phaser3 = MMORPG - Client et serveur

image

Dans le dernier article, nous avons fait un blanc avec vous, pour ainsi dire, la base sur laquelle notre univers sera créé, la visualisation à l'aide de la console peut sembler bonne, mais les caractères du texte sont ennuyeux et pas très beaux, dans cet article, nous nous concentrerons sur la visualisation de nos carreaux à l'aide Phaser.js

Dans un article précédent, notre projet ressemblait à ceci:

image

Maintenant, nous allons utiliser d'autres outils pour le développement web, j'espère que vous avez installé Node.js et npm, sinon installez-le de toute urgence. Et donc nous ouvrons le terminal et commençons:

$ npm install phaser@3.22.0

Une fois l'équipe terminée avec succès, nous devrions voir ce qui suit:

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

image

Tellement génial, des modules sont apparus, maintenant nous allons créer un répertoire pour notre client

image

Dans le contenu, nous allons stocker des ressources de jeu, c'est-à-dire nos sprites. Nous allons également créer deux fichiers game.js et MainScene.js, dans le répertoire racine (où se trouve le fichier
main.go ) créer index.html game.js - stocke les paramètres principaux du jeu
MainScene.js - contiendra la classe de la scène principale du jeu
index.html - la page où la scène sera rendue

Connectez immédiatement nos scripts à index.html et nous ne reviendrons plus sur ce fichier:

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

Dans MainScene.js, nous créerons un petit modèle de classe pour notre future scène:

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

}
create() {

}
update() {

}
}

Dans game.js ajoutez des paramètres typiques à votre goût, voici les miens:

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);

Maintenant, nous avons besoin d'un serveur HTTP, cela se fait en quelques lignes. Accédez à main.go et créez un serveur:

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())
	}
}

Eh bien, nous avons notre propre serveur Web et client! Commençons! Ouvrez la console:

$ go run main.go

Nous ouvrons le navigateur et essayons de nous connecter à notre serveur, dans mon cas c'est
localhost: 8080

image

Si vous avez vu un écran noir, vous avez tout fait correctement.

Et donc, créons un autre gestionnaire par lequel nous recevrons notre bloc au format json. Créez un répertoire séparé et appelez-le GameController, ici nous aurons tous les gestionnaires travaillant avec les données du jeu, créez le fichier Map_Controller.go

Nous avons également besoin d'une version améliorée

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
}


Nous venons d'ajouter des clés json à nos structures et d'améliorer un peu la création de morceaux. Nous
revenons à 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))
}

et ajoutez une ligne à main.go

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

Essayons de démarrer le serveur et allons sur localhost: 8080 / map.

Sortie dans le terminal:

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

Oui, nous avons oublié qu'à Golang, lors de la sérialisation, les clés de carte doivent être une chaîne. Pour la sérialisation, Go vérifie si le type correspond à l'interface TextMarshaler et appelle sa méthode MarshalText (), il nous suffit de créer la méthode MarshalText () pour notre type de coordonnées. Nous
revenons à Chunk.go et ajoutons le code suivant:

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

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

Vous pouvez écrire votre implémentation, le plus important est que cette méthode retourne une chaîne unique. Nous allons utiliser cette clé pour gérer les morceaux sur le client, maintenant vérifions le fonctionnement de notre contrôleur, réexécutons le serveur et voyons la sortie vers la console.

image

Oui, tout va bien, faisons maintenant une conclusion au flux, ajoutons deux lignes à la fin de notre contrôleur:


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

Pour l'instant, terminez avec Golang et revenez au client. nous aurons besoin de trois titres, bien qu'en fait nous en ayons 4, mais pour l'instant nous en avons trois, voire deux.







Ajoutez nos tuiles au répertoire Content et commencez à travailler avec MainScene.js, pour les premiers résultats nous avons besoin de quelques fonctions:

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)
    }
}

}

Le serveur nous rend notre morceau sous la forme d'un objet json, vous pouvez regarder sa structure dans la console du navigateur:

image

Et donc Phaser l'a rendu dans le navigateur:

image

Nous avons examiné le travail le plus simple entre le serveur et le client, dans le prochain article, nous allons immédiatement dessiner 9 morceaux et naviguer au monde. Voir tout le code et les ressources de l'article ici .

All Articles