Golang + Phaser3 = MMORPG - Client und Server

Bild

Im letzten Artikel haben wir mit Ihnen sozusagen ein Leerzeichen erstellt, auf dessen Grundlage unser Universum erstellt wird. Die Visualisierung mit der Konsole mag gut aussehen, aber Textzeichen sind langweilig und nicht sehr schön. In diesem Artikel konzentrieren wir uns auf die Visualisierung unserer Kacheln mit Phaser.js

Im letzten Artikel sah unser Projekt folgendermaßen aus:

Bild

Jetzt werden wir andere Tools für die Webentwicklung verwenden. Ich hoffe, Sie haben Node.js und npm installiert, wenn nicht, installieren Sie es dringend. Und so öffnen wir das Terminal und starten:

$ npm install phaser@3.22.0

Nach erfolgreichem Abschluss des Teams sollten wir Folgendes sehen:

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

Bild

Es sind so großartige Module erschienen, dass wir jetzt ein Verzeichnis für unseren Kunden erstellen.

Bild

In Inhalten werden wir Spielressourcen speichern, d. H. unsere Sprites. Wir werden auch zwei Dateien game.js und MainScene.js erstellen, im Stammverzeichnis (wo sich die Datei main.go befindet) create index.html
game.js - speichert die Haupteinstellungen für das Spiel
MainScene.js - es enthält die Klasse der Hauptspielszene
index.html - Die Seite, auf der die Szene gerendert wird.

Verbinden Sie unsere Skripte sofort mit index.html und wir werden nicht mehr zu dieser Datei zurückkehren:

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

In MainScene.js werden wir eine kleine Klassenvorlage für unsere zukünftige Szene erstellen:

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

}
create() {

}
update() {

}
}

Fügen Sie in game.js typische Einstellungen hinzu, die Ihnen gefallen. Hier sind meine:

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

Jetzt brauchen wir einen HTTP-Server, auf dem dies in wenigen Zeilen erfolgt. Gehen Sie zu main.go und erstellen Sie einen Server:

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

Nun, wir haben unseren eigenen Webserver und Client! Lass uns anfangen! Öffnen Sie die Konsole:

$ go run main.go

Wir öffnen den Browser und versuchen, eine Verbindung zu unserem Server herzustellen
localhost: 8080

Bild

Wenn Sie einen schwarzen Bildschirm gesehen haben, haben Sie alles richtig gemacht.

Erstellen wir also einen weiteren Handler, mit dem wir unseren Block im JSON-Format erhalten. Erstellen Sie ein separates Verzeichnis und nennen Sie es GameController. Hier arbeiten alle Handler mit den Spieldaten. Erstellen Sie die Datei Map_Controller.go.

Außerdem benötigen wir eine verbesserte

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
}


Wir haben gerade json-Schlüssel zu unseren Strukturen hinzugefügt und die Chunk-Erstellung ein wenig verbessert. Wir
kehren zu Map_Controller zurück.

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

und fügen Sie eine Zeile zu main.go hinzu

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

Versuchen wir, den Server zu starten und gehen Sie zu localhost: 8080 / map.

Ausgabe im Terminal:

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

Ja, wir haben vergessen, dass in Golang bei der Serialisierung die Kartenschlüssel eine Zeichenfolge sein müssen. Für die Serialisierung prüft Go, ob der Typ mit der TextMarshaler-Schnittstelle übereinstimmt, und ruft die MarshalText () -Methode auf. Wir müssen lediglich die MarshalText () -Methode für unseren Koordinatentyp erstellen. Wir
kehren zu Chunk.go zurück und fügen den folgenden Code hinzu:

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

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

Sie können Ihre Implementierung schreiben. Das Wichtigste ist, dass diese Methode eine eindeutige Zeichenfolge zurückgibt. Wir werden diesen Schlüssel verwenden, um Chunks auf dem Client zu verwalten. Lassen Sie uns nun überprüfen, wie unser Controller funktioniert, den Server erneut starten und die Ausgabe an die Konsole anzeigen.

Bild

Ja, alles ist in Ordnung. Lassen Sie uns nun einen Abschluss für den Stream ziehen und zwei Zeilen am Ende unseres Controllers hinzufügen:


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

Beenden Sie vorerst Golang und kehren Sie zum Kunden zurück. Wir werden drei Titel brauchen, obwohl wir tatsächlich vier davon haben, aber im Moment haben wir drei oder sogar zwei.







Fügen Sie unsere Kacheln zum Inhaltsverzeichnis hinzu und beginnen Sie mit MainScene.js zu arbeiten. Für die ersten Ergebnisse benötigen wir einige Funktionen:

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

}

Der Server gibt unseren Chunk in Form eines JSON-Objekts an uns zurück. Sie können seine Struktur in der Browserkonsole anzeigen:

Bild

Und so hat Phaser ihn im Browser gerendert:

Bild

Wir haben die einfachste Arbeit zwischen Server und Client untersucht. Im nächsten Artikel werden wir sofort 9 Chunks zeichnen und navigieren zur Welt. Den gesamten Code und die Ressourcen aus dem Artikel finden Sie hier .

All Articles