Browse Source

Add dependency GGPO-Go, which provides rollback networking

wip
Trevor Slocum 4 weeks ago
parent
commit
c90a2ab605
  1. 1
      README.md
  2. 46
      flags.go
  3. 15
      flags_web.go
  4. 261
      game/game.go
  5. 90
      game/inputs.go
  6. 19
      game/player.go
  7. 102
      game/session.go
  8. 10
      go.mod
  9. 20
      go.sum
  10. 7
      main.go
  11. 144
      system/ui.go
  12. 6
      world/world.go

1
README.md

@ -28,5 +28,6 @@ Please share issues and suggestions [here](https://code.rocketnine.space/tslocum
## Dependencies
- [ebitengine](https://github.com/hajimehoshi/ebiten) - Game engine
- [ggpo-go](https://github.com/assemblaj/ggpo) - Rollback networking library
- [gohan](https://code.rocketnine.space/tslocum/gohan) - Entity Component System framework
- [etk](https://code.rocketnine.space/tslocum/etk) - Graphical User Interface toolkit

46
flags.go

@ -0,0 +1,46 @@
//go:build !js || !wasm
// +build !js !wasm
package main
import (
"flag"
"log"
"os"
"code.rocketnine.space/tslocum/boxbrawl/world"
"github.com/assemblaj/ggpo"
"github.com/hajimehoshi/ebiten/v2"
)
func parseFlags() {
var (
fullscreen bool
hostAddress string
connectAddress string
printDebug bool
)
flag.BoolVar(&fullscreen, "fullscreen", false, "run in fullscreen mode")
flag.StringVar(&hostAddress, "host", "", "start hosting a match on specified address:port")
flag.StringVar(&connectAddress, "connect", "", "connect to a match at specified address:port")
flag.IntVar(&world.LocalPort, "local", 0, "set local port (this is not normally required)")
flag.BoolVar(&printDebug, "debug", false, "enable printing debug messages")
flag.Parse()
if fullscreen {
ebiten.SetFullscreen(true)
}
if printDebug {
ggpo.SetLogger(log.New(os.Stderr, "GGPO ", log.Ldate|log.Ltime|log.Lmsgprefix))
}
if hostAddress != "" {
world.ConnectPromptHost = true
world.ConnectPromptText = hostAddress
world.ConnectPromptConfirmed = true
} else if connectAddress != "" {
world.ConnectPromptText = connectAddress
world.ConnectPromptConfirmed = true
}
}

15
flags_web.go

@ -0,0 +1,15 @@
//go:build js && wasm
// +build js,wasm
package main
import (
"code.rocketnine.space/tslocum/boxbrawl/world"
"github.com/hajimehoshi/ebiten/v2"
)
func parseFlags() {
world.WASM = true
ebiten.SetFullscreen(true)
}

261
game/game.go

@ -1,29 +1,69 @@
package game
import (
"crypto/sha1"
"image/color"
"log"
"os"
"strconv"
"strings"
"code.rocketnine.space/tslocum/boxbrawl/entity"
"code.rocketnine.space/tslocum/boxbrawl/system"
"code.rocketnine.space/tslocum/boxbrawl/world"
"code.rocketnine.space/tslocum/etk"
"code.rocketnine.space/tslocum/gohan"
"github.com/assemblaj/ggpo"
"github.com/hajimehoshi/ebiten/v2"
)
type Game struct{}
var backend ggpo.Backend
func NewGame() (*Game, error) {
g := &Game{}
var currentPlayer = 1
entity.NewOnceEntity()
type Game struct {
Players []Player
}
gohan.AddSystem(&system.UISystem{})
var addedGame bool
func NewGame() (*Game, error) {
var player1 = Player{
X: 50,
Y: 50,
Color: color.RGBA{255, 0, 0, 255},
PlayerNum: 1,
}
var player2 = Player{
X: 150,
Y: 50,
Color: color.RGBA{0, 0, 255, 255},
PlayerNum: 2,
}
g := &Game{
Players: []Player{player1, player2},
}
if !addedGame {
entity.NewOnceEntity()
gohan.AddSystem(&system.UISystem{})
addedGame = true
}
return g, nil
}
func (g *Game) clone() (result *Game) {
result = &Game{}
*result = *g
result.Players = make([]Player, len(g.Players))
for i := range g.Players {
result.Players[i] = g.Players[i].clone()
}
return
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
if outsideWidth != world.ScreenWidth || outsideHeight != world.ScreenHeight {
if world.ScreenWidth != 0 || world.ScreenHeight != 0 {
@ -35,11 +75,71 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeigh
}
func (g *Game) Update() error {
if ebiten.IsWindowBeingClosed() {
if ebiten.IsWindowBeingClosed() || (!world.WASM && ebiten.IsKeyPressed(ebiten.KeyEscape)) {
g.Exit()
return nil
}
if world.ConnectPromptConfirmed && !world.ConnectPromptActive {
address := ""
port := world.ConnectPromptText
if strings.ContainsRune(port, ':') {
split := strings.Split(port, ":")
if len(split) == 2 {
address = split[0]
port = split[1]
}
}
p, err := strconv.Atoi(port)
if err != nil {
log.Fatalf("failed to read port: %s", err)
}
log.Println("start networking")
localPort := world.LocalPort
if localPort == 0 {
localPort = p
}
numPlayers := 2
playerSize := 20
players := make([]ggpo.Player, numPlayers)
if world.ConnectPromptHost {
log.Printf("Hosting at " + address + ":" + port + "...")
players[0] = ggpo.NewLocalPlayer(playerSize, 1)
players[1] = ggpo.NewRemotePlayer(playerSize, 2, "127.0.0.1", localPort)
} else {
log.Printf("Connecting to " + address + ":" + port + "...")
players[0] = ggpo.NewRemotePlayer(playerSize, 1, address, p)
players[1] = ggpo.NewLocalPlayer(playerSize, 2)
currentPlayer = 2
}
if world.LocalPort != 0 {
log.Printf("Client port for connection: %d", world.LocalPort)
}
l := p
if !world.ConnectPromptHost {
l = localPort
}
g.InitNetworking(l, numPlayers, players, 0)
world.ConnectPromptActive = true
}
if world.ConnectPromptActive {
err := backend.Idle(0) // TODO Why 0?
if err != nil {
panic(err)
}
g.RunFrame()
}
return gohan.Update()
}
@ -53,3 +153,152 @@ func (g *Game) Draw(screen *ebiten.Image) {
func (g *Game) Exit() {
os.Exit(0)
}
func (g *Game) InitNetworking(localPort int, numPlayers int, players []ggpo.Player, numSpectators int) {
var result error
var inputBits InputBits = 0
var inputSize int = len(encodeInputs(inputBits))
session := NewGameSession()
peer := ggpo.NewPeer(session, localPort, numPlayers, inputSize)
//peer := ggpo.NewSyncTest(&session, numPlayers, 8, inputSize, true)
backend = &peer
session.backend = backend
peer.InitializeConnection()
peer.Start()
//session.SetDisconnectTimeout(3000)
//session.SetDisconnectNotifyStart(1000)
for i := 0; i < numPlayers+numSpectators; i++ {
var handle ggpo.PlayerHandle
result = peer.AddPlayer(&players[i], &handle)
if players[i].PlayerType == ggpo.PlayerTypeLocal {
currentPlayer = int(handle)
}
if result != nil {
log.Fatalf("There's an issue from AddPlayer")
}
if players[i].PlayerType == ggpo.PlayerTypeLocal {
peer.SetFrameDelay(handle, FRAME_DELAY)
}
}
peer.SetDisconnectTimeout(3000)
peer.SetDisconnectNotifyStart(1000)
}
func (g *Game) RunFrame() {
input := g.ReadInputs()
buffer := encodeInputs(input)
//fmt.Println("Attempting to add local inputs")
result := backend.AddLocalInput(ggpo.PlayerHandle(currentPlayer), buffer, len(buffer))
//fmt.Println("Attempt to add local inputs complete")
if result == nil {
//fmt.Println("Attempt to add local inputs was successful")
var values [][]byte
disconnectFlags := 0
//fmt.Println("Attempting to synchronize inputs")
values, result = backend.SyncInput(&disconnectFlags)
if result == nil {
//fmt.Println("Attempt synchronize inputs was sucessful")
inputs := decodeInputs(values)
//fmt.Println("Advancing Frame from game loop")
g.AdvanceFrame(inputs, disconnectFlags)
} else {
//fmt.Printf("Attempt synchronize inputs was unsuccessful: %s\n", result)
}
} else {
//fmt.Printf("Attempt to add local inputs unsuccessful: %s\n", result)
}
}
func (g *Game) AdvanceFrame(inputs []InputBits, disconnectFlags int) {
log.Println("ADVANCE FRAME")
g.UpdateByInputs(inputs)
err := backend.AdvanceFrame(uint32(g.Checksum()))
if err != nil {
panic(err)
}
}
func (g *Game) UpdateByInputs(inputs []InputBits) {
for i, input := range inputs {
if input.isButtonOn(int(ebiten.KeyArrowUp)) {
g.Players[i].Y--
}
if input.isButtonOn(int(ebiten.KeyArrowDown)) {
g.Players[i].Y++
}
if input.isButtonOn(int(ebiten.KeyArrowLeft)) {
g.Players[i].X--
}
if input.isButtonOn(int(ebiten.KeyArrowRight)) {
g.Players[i].X++
}
if input.isButtonOn(int(ebiten.KeyW)) {
g.Players[i].Y--
}
if input.isButtonOn(int(ebiten.KeyS)) {
g.Players[i].Y++
}
if input.isButtonOn(int(ebiten.KeyA)) {
g.Players[i].X--
}
if input.isButtonOn(int(ebiten.KeyD)) {
g.Players[i].X++
}
}
}
func (g *Game) ReadInputs() InputBits {
var in InputBits
if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
in.setButton(int(ebiten.KeyArrowUp))
}
if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
in.setButton(int(ebiten.KeyArrowDown))
}
if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
in.setButton(int(ebiten.KeyArrowLeft))
}
if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
in.setButton(int(ebiten.KeyArrowRight))
}
return in
}
func (g *Game) ReadInputsP2() InputBits {
var in InputBits
if ebiten.IsKeyPressed(ebiten.KeyW) {
in.setButton(int(ebiten.KeyW))
}
if ebiten.IsKeyPressed(ebiten.KeyS) {
in.setButton(int(ebiten.KeyS))
}
if ebiten.IsKeyPressed(ebiten.KeyA) {
in.setButton(int(ebiten.KeyA))
}
if ebiten.IsKeyPressed(ebiten.KeyD) {
in.setButton(int(ebiten.KeyD))
}
return in
}
func (g *Game) Checksum() int {
h := sha1.New()
h.Write([]byte(g.String()))
toSum := h.Sum(nil)
sum := 0
for _, v := range toSum {
sum += int(v)
}
return sum
}

90
game/inputs.go

@ -0,0 +1,90 @@
package game
import (
"bytes"
"encoding/gob"
"fmt"
"log"
)
type Input struct {
ButtonMap int
}
type InputBits int
func (i *InputBits) isButtonOn(button int) bool {
return *i&(1<<button) > 0
}
func (i *InputBits) setButton(button int) {
*i |= (1 << button)
}
func readI32(b []byte) int32 {
if len(b) < 4 {
return 0
}
return int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24
}
func writeI32(i32 int32) []byte {
b := []byte{byte(i32), byte(i32 >> 8), byte(i32 >> 16), byte(i32 >> 24)}
return b
}
func (i *Input) isButtonOn(button int) bool {
return i.ButtonMap&(1<<button) > 0
}
func (i *Input) setButton(button int) {
i.ButtonMap |= (1 << button)
}
func (i Input) String() string {
return fmt.Sprintf("Input %d", i.ButtonMap)
}
func NewInput() Input {
return Input{}
}
func encodeInputs(inputs InputBits) []byte {
return writeI32(int32(inputs))
}
func decodeInputs(buffer [][]byte) []InputBits {
var inputs = make([]InputBits, len(buffer))
for i, b := range buffer {
inputs[i] = InputBits(readI32(b))
}
return inputs
}
func decodeInputsGob(buffer [][]byte) []Input {
var inputs = make([]Input, len(buffer))
for i, b := range buffer {
var buf bytes.Buffer = *bytes.NewBuffer(b)
dec := gob.NewDecoder(&buf)
err := dec.Decode(&inputs[i])
if err != nil {
log.Printf("decode error: %s. Returning empty input\n", err)
// hack
inputs[i] = NewInput()
//panic("eof")
} else {
log.Printf("inputs properly decoded: %s\n", inputs[i])
}
}
return inputs
}
func encodeInputsGob(inputs Input) []byte {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(&inputs)
if err != nil {
log.Fatal("encode error ", err)
}
return buf.Bytes()
}

19
game/player.go

@ -0,0 +1,19 @@
package game
import "image/color"
type Player struct {
X float64
Y float64
Color color.Color
PlayerNum int
}
func (p *Player) clone() Player {
result := Player{}
result.X = p.X
result.Y = p.Y
result.Color = p.Color
result.PlayerNum = p.PlayerNum
return result
}

102
game/session.go

@ -0,0 +1,102 @@
package game
import (
"bytes"
"crypto/md5"
"encoding/gob"
"fmt"
"log"
"github.com/assemblaj/ggpo"
)
const FRAME_DELAY int = 2
type GameSession struct {
backend ggpo.Backend
game *Game
saveStates map[int]*Game
}
func NewGameSession() *GameSession {
g := &GameSession{}
game, _ := NewGame()
g.game = game
g.saveStates = make(map[int]*Game)
return g
}
func (g *GameSession) SaveGameState(stateID int) int {
g.saveStates[stateID] = g.game.clone()
checksum := calculateChecksum([]byte(g.saveStates[stateID].String()))
return checksum
}
func calculateChecksum(buffer []byte) int {
cSum := md5.Sum(buffer)
checksum := 0
for i := 0; i < len(cSum); i++ {
checksum += int(cSum[i])
}
return checksum
}
func (g *GameSession) LoadGameState(stateID int) {
*g.game = *g.saveStates[stateID]
}
func (g *GameSession) LogGameState(fileName string, buffer []byte, len int) {
var game2 Game
var buf bytes.Buffer = *bytes.NewBuffer(buffer)
dec := gob.NewDecoder(&buf)
err := dec.Decode(&game2)
if err != nil {
log.Fatal("decode error:", err)
}
log.Printf("%s Game State: %s\n", fileName, game2.String())
}
func (g *GameSession) SetBackend(backend ggpo.Backend) {
}
func (g *Game) String() string {
return fmt.Sprintf("%s : %s ", g.Players[0].String(), g.Players[1].String())
}
func (p *Player) String() string {
return fmt.Sprintf("Player %d: X:%f Y:%f Color: %s", p.PlayerNum, p.X, p.Y, p.Color)
}
func (g *GameSession) AdvanceFrame(flags int) {
fmt.Println("Advancing frame from callback. ")
var discconectFlags int
// Make sure we fetch the inputs from GGPO and use these to update
// the game state instead of reading from the keyboard.
inputs, result := g.backend.SyncInput(&discconectFlags)
if result == nil {
input := decodeInputs(inputs)
g.game.AdvanceFrame(input, discconectFlags)
}
}
func (g *GameSession) OnEvent(info *ggpo.Event) {
switch info.Code {
case ggpo.EventCodeConnectedToPeer:
log.Println("EventCodeConnectedToPeer")
case ggpo.EventCodeSynchronizingWithPeer:
log.Println("EventCodeSynchronizingWithPeer")
case ggpo.EventCodeSynchronizedWithPeer:
log.Println("EventCodeSynchronizedWithPeer")
case ggpo.EventCodeRunning:
log.Println("EventCodeRunning")
case ggpo.EventCodeDisconnectedFromPeer:
log.Println("EventCodeDisconnectedFromPeer")
case ggpo.EventCodeTimeSync:
log.Println("EventCodeTimeSync")
case ggpo.EventCodeConnectionInterrupted:
log.Println("EventCodeconnectionInterrupted")
case ggpo.EventCodeConnectionResumed:
log.Println("EventCodeconnectionInterrupted")
}
}

10
go.mod

@ -5,6 +5,7 @@ go 1.19
require (
code.rocketnine.space/tslocum/etk v0.0.0-20230103193701-368514415e01
code.rocketnine.space/tslocum/gohan v1.0.0
github.com/assemblaj/ggpo v0.0.0-20230105182823-b13b11d28a8e
github.com/hajimehoshi/ebiten/v2 v2.4.15
)
@ -14,9 +15,10 @@ require (
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/hajimehoshi/file2byteslice v1.0.0 // indirect
github.com/jezek/xgb v1.1.0 // indirect
golang.org/x/exp/shiny v0.0.0-20221230185412-738e83a70c30 // indirect
golang.org/x/image v0.2.0 // indirect
golang.org/x/exp v0.0.0-20230105000112-eab7a2c85304 // indirect
golang.org/x/exp/shiny v0.0.0-20230105000112-eab7a2c85304 // indirect
golang.org/x/image v0.3.0 // indirect
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
)

20
go.sum

@ -5,6 +5,8 @@ code.rocketnine.space/tslocum/gohan v1.0.0/go.mod h1:12yOt5Ygl/RVwnnZSVZRuS1W6gC
code.rocketnine.space/tslocum/messeji v1.0.2 h1:3/68FnXWaBDMhfUGb8FvNpVgAHY8DX+VL7pyA/CcY94=
code.rocketnine.space/tslocum/messeji v1.0.2/go.mod h1:bSXsyjvKhFXQ7GsUxWZdO2JX83xOT/VTqFCR04thk+c=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/assemblaj/ggpo v0.0.0-20230105182823-b13b11d28a8e h1:d/uJpkRgHj5EJ06CX0TFFQLfvAeHlfWY+xfLUBkVeus=
github.com/assemblaj/ggpo v0.0.0-20230105182823-b13b11d28a8e/go.mod h1:ZKiAYEZgxDlGHGeP/VZsv1+xIRo9kQpgUFmjP/PR0lQ=
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/ebitengine/purego v0.1.1 h1:HI8nW+LniW9Yb34k34jBs8nz+PNzsw68o7JF8jWFHHE=
github.com/ebitengine/purego v0.1.1/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
@ -36,13 +38,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp/shiny v0.0.0-20221230185412-738e83a70c30 h1:GoCh95fUWm4yMPxPDXFjbjwUFoO+RJy052MW5+PWh3s=
golang.org/x/exp/shiny v0.0.0-20221230185412-738e83a70c30/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/exp v0.0.0-20230105000112-eab7a2c85304 h1:YUqj+XKtfrn3kXjFIiZ8jwKROD7ioAOOHUuo3ZZ2opc=
golang.org/x/exp v0.0.0-20230105000112-eab7a2c85304/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp/shiny v0.0.0-20230105000112-eab7a2c85304 h1:ezMmyIKsGPwRz+IHa53wCpw87I2TremhqQ8o79ytDEk=
golang.org/x/exp/shiny v0.0.0-20230105000112-eab7a2c85304/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ=
golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI=
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
@ -73,8 +77,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -82,8 +86,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

7
main.go

@ -20,18 +20,13 @@ func main() {
ebiten.SetTPS(world.TPS)
ebiten.SetRunnableOnUnfocused(true)
//parseFlags()
parseFlags()
g, err := game.NewGame()
if err != nil {
log.Fatal(err)
}
// TODO
if false {
world.ConnectPromptVisible = true
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGINT,

144
system/ui.go

@ -2,7 +2,6 @@ package system
import (
"fmt"
"log"
"code.rocketnine.space/tslocum/boxbrawl/component"
"code.rocketnine.space/tslocum/boxbrawl/world"
@ -17,8 +16,15 @@ const uiStartPrompt = `Box Brawl
`
const uiComputerPrompt = `Press <Enter> to play against the computer.`
const uiHostPrompt = `Press <H> to host a match against a remote player.`
const uiHostInfoPrompt = `Type a port to host a match against a remote player.`
const uiHostStartPrompt = `Press <Enter> to start hosting.`
const uiRemotePrompt = `Type an IP address and port (address:port) to play against a remote player.`
const uiConnectPrompt = `Press <Enter> to connect.`
const uiBrowserPrompt = `Playing against remote players is unavailable in the browser version.
Download Box Brawl for Windows or Linux to play against remote players.`
const uiHostListeningPrompt = `Waiting for a connection on %s...`
const uiClientConnectingPrompt = `Connecting to %s...`
type UISystem struct {
*component.Once
@ -38,7 +44,6 @@ func (u *UISystem) initialize() {
inputDemo.AddChild(u.buffer)
etk.SetRoot(inputDemo)
etk.Layout(world.ScreenWidth, world.ScreenHeight)
u.debugImg = ebiten.NewImage(128, 128)
@ -49,15 +54,31 @@ func (u *UISystem) initialize() {
func (u *UISystem) updateBuffer() {
prompt := []byte(uiStartPrompt)
promptEntered := len(world.ConnectPromptText) != 0
if promptEntered {
prompt = append(prompt, []byte("\n\n"+world.ConnectPromptText)...)
prompt = append(prompt, []byte("\n\n"+uiConnectPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
} else {
if world.WASM {
prompt = append(prompt, []byte("\n\n"+uiComputerPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiHostPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
prompt = append(prompt, []byte("\n\n"+uiBrowserPrompt)...)
} else if world.ConnectPromptActive {
if world.ConnectPromptHost {
prompt = append(prompt, []byte("\n\n"+fmt.Sprintf(uiHostListeningPrompt, world.ConnectPromptText))...)
} else {
prompt = append(prompt, []byte("\n\n"+fmt.Sprintf(uiClientConnectingPrompt, world.ConnectPromptText))...)
}
} else {
promptEntered := len(world.ConnectPromptText) != 0
if promptEntered || world.ConnectPromptHost {
prompt = append(prompt, []byte("\n\n"+world.ConnectPromptText)...)
if world.ConnectPromptHost {
prompt = append(prompt, []byte("\n\n"+uiHostInfoPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiHostStartPrompt)...)
} else {
prompt = append(prompt, []byte("\n\n"+uiConnectPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
}
} else {
prompt = append(prompt, []byte("\n\n"+uiComputerPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiHostPrompt)...)
prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...)
}
}
u.buffer.Clear()
@ -73,55 +94,66 @@ func (u *UISystem) Update(e gohan.Entity) error {
return nil
}
var a string
if inpututil.IsKeyJustPressed(ebiten.KeyDigit0) {
a += "0"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit1) {
a += "1"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit2) {
a += "2"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit3) {
a += "3"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit4) {
a += "4"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit5) {
a += "5"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit6) {
a += "6"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit7) {
a += "7"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit8) {
a += "8"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit9) {
a += "9"
}
if inpututil.IsKeyJustPressed(ebiten.KeyPeriod) {
a += "."
}
if inpututil.IsKeyJustPressed(ebiten.KeySemicolon) && ebiten.IsKeyPressed(ebiten.KeyShift) {
a += ":"
}
if a != "" {
world.ConnectPromptText += a
u.updateBuffer()
}
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(world.ConnectPromptText) != 0 {
world.ConnectPromptText = world.ConnectPromptText[:len(world.ConnectPromptText)-1]
u.updateBuffer()
if !world.WASM {
if len(world.ConnectPromptText) == 0 && ebiten.IsKeyPressed(ebiten.KeyH) {
world.ConnectPromptHost = true
u.updateBuffer()
}
var a string
if inpututil.IsKeyJustPressed(ebiten.KeyDigit0) {
a += "0"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit1) {
a += "1"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit2) {
a += "2"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit3) {
a += "3"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit4) {
a += "4"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit5) {
a += "5"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit6) {
a += "6"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit7) {
a += "7"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit8) {
a += "8"
}
if inpututil.IsKeyJustPressed(ebiten.KeyDigit9) {
a += "9"
}
if inpututil.IsKeyJustPressed(ebiten.KeyPeriod) {
a += "."
}
if inpututil.IsKeyJustPressed(ebiten.KeySemicolon) && ebiten.IsKeyPressed(ebiten.KeyShift) {
a += ":"
}
if a != "" {
world.ConnectPromptText += a
u.updateBuffer()
}
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
if len(world.ConnectPromptText) != 0 {
world.ConnectPromptText = world.ConnectPromptText[:len(world.ConnectPromptText)-1]
u.updateBuffer()
} else if world.ConnectPromptHost {
world.ConnectPromptHost = false
u.updateBuffer()
}
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyKPEnter) {
log.Println("Start game")
return nil
world.ConnectPromptConfirmed = true
}
return etk.Update()

6
world/world.go

@ -10,9 +10,15 @@ const (
var (
ScreenWidth, ScreenHeight = 0, 0
LocalPort int
ConnectPromptVisible = true
ConnectPromptText string
ConnectPromptHost bool
ConnectPromptConfirmed bool
ConnectPromptActive bool
Debug = 1
WASM bool
)

Loading…
Cancel
Save