ECS
This commit is contained in:
parent
fff70cc8b3
commit
87f5c2f842
14 changed files with 153 additions and 54 deletions
|
@ -7,6 +7,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/server"
|
"code.haedhutner.dev/mvv/LastMUD/internal/server"
|
||||||
|
|
||||||
|
@ -57,5 +58,7 @@ func processInput() {
|
||||||
if buf[0] == 'q' {
|
if buf[0] == 'q' {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
internal/game/.DS_Store
vendored
Normal file
BIN
internal/game/.DS_Store
vendored
Normal file
Binary file not shown.
17
internal/game/db/repository.go
Normal file
17
internal/game/db/repository.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type Identifier = uuid.UUID
|
||||||
|
|
||||||
|
type Entity interface {
|
||||||
|
Id() Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
type Repository[T Entity] interface {
|
||||||
|
Create(entity T) (rowsAffected int, err error)
|
||||||
|
Delete(entity T) (rowsAffected int, err error)
|
||||||
|
Update(entity T) (rowsAffected int, err error)
|
||||||
|
FetchOne(id Identifier) (entity T, err error)
|
||||||
|
FetchAll() (entities []T, err error)
|
||||||
|
}
|
31
internal/game/ecs/components.go
Normal file
31
internal/game/ecs/components.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
type PlayerState = byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerStateJoining PlayerState = iota
|
||||||
|
PlayerStateLoggingIn
|
||||||
|
PlayerStateRegistering
|
||||||
|
PlayerStatePlaying
|
||||||
|
PlayerStateLeaving
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlayerStateComponent struct {
|
||||||
|
State PlayerState
|
||||||
|
}
|
||||||
|
|
||||||
|
type NameComponent struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DescriptionComponent struct {
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type InRoomComponent struct {
|
||||||
|
InRoom Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
type NeighboringRoomsComponent struct {
|
||||||
|
North, South, East, West Entity
|
||||||
|
}
|
19
internal/game/ecs/entity.go
Normal file
19
internal/game/ecs/entity.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type Entity uuid.UUID
|
||||||
|
|
||||||
|
type Room struct {
|
||||||
|
Entity
|
||||||
|
NameComponent
|
||||||
|
DescriptionComponent
|
||||||
|
NeighboringRoomsComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
Entity
|
||||||
|
PlayerStateComponent
|
||||||
|
NameComponent
|
||||||
|
InRoomComponent
|
||||||
|
}
|
1
internal/game/ecs/systems.go
Normal file
1
internal/game/ecs/systems.go
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package ecs
|
15
internal/game/ecs/world.go
Normal file
15
internal/game/ecs/world.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package ecs
|
||||||
|
|
||||||
|
type World struct {
|
||||||
|
Players []*Player
|
||||||
|
Rooms []*Room
|
||||||
|
DefaultRoom *Room
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWorld() *World {
|
||||||
|
world := &World{
|
||||||
|
Players: []*Player{},
|
||||||
|
Rooms: []*Room{},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -45,9 +45,9 @@ type EventBus struct {
|
||||||
events chan GameEvent
|
events chan GameEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateEventBus() *EventBus {
|
func CreateEventBus(capacity int) *EventBus {
|
||||||
return &EventBus{
|
return &EventBus{
|
||||||
events: make(chan GameEvent, 10),
|
events: make(chan GameEvent, capacity),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,10 @@ func (pje *PlayerJoinEvent) Type() EventType {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||||
game.world.AddPlayerToDefaultRoom(CreatePlayer(pje.connectionId, nil))
|
p := CreateJoiningPlayer(pje.connectionId)
|
||||||
game.enqeueOutput(game.CreateOutput(pje.connectionId, []byte("Welcome to LastMUD!")))
|
game.world.AddPlayerToDefaultRoom(p)
|
||||||
|
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Welcome to LastMUD!")))
|
||||||
|
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Please enter your name:")))
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerLeaveEvent struct {
|
type PlayerLeaveEvent struct {
|
||||||
|
@ -51,7 +53,7 @@ type PlayerCommandEvent struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) {
|
func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) {
|
||||||
cmdCtx, err := command.CreateCommandContext(game.CommandRegistry(), cmdString)
|
cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -77,17 +79,23 @@ func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event := pce.parseCommandIntoEvent(game, player)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player *Player) GameEvent {
|
||||||
switch pce.command.Command().Definition().Name() {
|
switch pce.command.Command().Definition().Name() {
|
||||||
case SayCommand:
|
case SayCommand:
|
||||||
speech, err := pce.command.Command().Parameters()[0].AsString()
|
speech, err := pce.command.Command().Parameters()[0].AsString()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error())
|
logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error())
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
game.EnqueueEvent(game.CreatePlayerSayEvent(player, speech))
|
return game.CreatePlayerSayEvent(player, speech)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerSayEvent struct {
|
type PlayerSayEvent struct {
|
||||||
|
|
|
@ -12,6 +12,9 @@ import (
|
||||||
|
|
||||||
const TickRate = time.Duration(50 * time.Millisecond)
|
const TickRate = time.Duration(50 * time.Millisecond)
|
||||||
|
|
||||||
|
const MaxEnqueuedOutputPerTick = 100
|
||||||
|
const MaxEnqueuedGameEventsPerTick = 100
|
||||||
|
|
||||||
type GameOutput struct {
|
type GameOutput struct {
|
||||||
connId uuid.UUID
|
connId uuid.UUID
|
||||||
contents []byte
|
contents []byte
|
||||||
|
@ -36,7 +39,8 @@ type LastMUDGame struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
|
|
||||||
commandRegistry *command.CommandRegistry
|
cmdRegistry *command.CommandRegistry
|
||||||
|
|
||||||
world *World
|
world *World
|
||||||
|
|
||||||
eventBus *EventBus
|
eventBus *EventBus
|
||||||
|
@ -48,12 +52,12 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
||||||
game = &LastMUDGame{
|
game = &LastMUDGame{
|
||||||
wg: wg,
|
wg: wg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
eventBus: CreateEventBus(),
|
eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick),
|
||||||
output: make(chan GameOutput, 10),
|
output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
|
||||||
world: CreateWorld(),
|
world: CreateWorld(),
|
||||||
}
|
}
|
||||||
|
|
||||||
game.commandRegistry = game.CreateGameCommandRegistry()
|
game.cmdRegistry = game.CreateGameCommandRegistry()
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go game.start()
|
go game.start()
|
||||||
|
@ -61,6 +65,23 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
|
||||||
|
game.eventBus.Push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
||||||
|
select {
|
||||||
|
case output := <-game.output:
|
||||||
|
return &output
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *LastMUDGame) commandRegistry() *command.CommandRegistry {
|
||||||
|
return game.cmdRegistry
|
||||||
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) start() {
|
func (game *LastMUDGame) start() {
|
||||||
defer game.wg.Done()
|
defer game.wg.Done()
|
||||||
defer game.shutdown()
|
defer game.shutdown()
|
||||||
|
@ -102,27 +123,10 @@ func (game *LastMUDGame) shouldStop() bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
|
|
||||||
game.eventBus.Push(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (game *LastMUDGame) enqeueOutput(output GameOutput) {
|
func (game *LastMUDGame) enqeueOutput(output GameOutput) {
|
||||||
game.output <- output
|
game.output <- output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
|
||||||
select {
|
|
||||||
case output := <-game.output:
|
|
||||||
return &output
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (game *LastMUDGame) CommandRegistry() *command.CommandRegistry {
|
|
||||||
return game.commandRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *LastMUDGame) tick(delta time.Duration) {
|
func (g *LastMUDGame) tick(delta time.Duration) {
|
||||||
for {
|
for {
|
||||||
event := g.eventBus.Pop()
|
event := g.eventBus.Pop()
|
||||||
|
|
|
@ -5,12 +5,23 @@ import "github.com/google/uuid"
|
||||||
type Player struct {
|
type Player struct {
|
||||||
id uuid.UUID
|
id uuid.UUID
|
||||||
|
|
||||||
|
state PlayerState
|
||||||
|
|
||||||
currentRoom *Room
|
currentRoom *Room
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlayer(identity uuid.UUID, room *Room) *Player {
|
func CreateJoiningPlayer(identity uuid.UUID) *Player {
|
||||||
return &Player{
|
return &Player{
|
||||||
id: identity,
|
id: identity,
|
||||||
|
state: PlayerStateJoining,
|
||||||
|
currentRoom: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePlayer(identity uuid.UUID, state PlayerState, room *Room) *Player {
|
||||||
|
return &Player{
|
||||||
|
id: identity,
|
||||||
|
state: state,
|
||||||
currentRoom: room,
|
currentRoom: room,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
package game
|
|
|
@ -11,7 +11,11 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MaxLastSeenTime = 120 * time.Second
|
const MaxLastSeenTime = 90 * time.Second
|
||||||
|
|
||||||
|
const CheckAlivePeriod = 50 * time.Millisecond
|
||||||
|
|
||||||
|
const DeleteBeforeAndMoveToStartOfLine = "\033[1K\r"
|
||||||
|
|
||||||
type Connection struct {
|
type Connection struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -53,6 +57,13 @@ func (c *Connection) Id() uuid.UUID {
|
||||||
return c.identity
|
return c.identity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Write(output []byte) (err error) {
|
||||||
|
output = append([]byte(DeleteBeforeAndMoveToStartOfLine+"< "), output...)
|
||||||
|
output = append(output, []byte("\n> ")...)
|
||||||
|
_, err = c.conn.Write(output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Connection) listen() {
|
func (c *Connection) listen() {
|
||||||
defer c.wg.Done()
|
defer c.wg.Done()
|
||||||
|
|
||||||
|
@ -100,6 +111,8 @@ func (c *Connection) checkAlive() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(CheckAlivePeriod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,10 +132,3 @@ func (c *Connection) closeConnection() {
|
||||||
|
|
||||||
logging.Info("Disconnected: ", c.conn.RemoteAddr())
|
logging.Info("Disconnected: ", c.conn.RemoteAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) Write(output []byte) (err error) {
|
|
||||||
output = append([]byte("< "), output...)
|
|
||||||
output = append(output, []byte("\n> ")...)
|
|
||||||
_, err = c.conn.Write(output)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
type inputEmptyError struct {
|
|
||||||
msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInputEmptyError() *inputEmptyError {
|
|
||||||
return &inputEmptyError{
|
|
||||||
msg: "No input available at this moment",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *inputEmptyError) Error() string {
|
|
||||||
return err.msg
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue