From e141203c4b3b086c27d59f1dfdf7442733a132d2 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 23 Jul 2018 20:10:53 -0700 Subject: [PATCH] Basic Collision Detection, Toggle Between Play/Edit Known bugs: * The Pixel format in the Grid has DX and DY attributes and it wreaks havoc on collision detection in Play Mode when you come straight from the editor. Reloading the map from disk to play is OK cuz it lacks these attrs. --- balance/shell.go | 4 +- doodads/doodads.go | 113 ++++++++++++++++++++++++++++++++++ doodads/player.go | 70 +++++++++++++++++++++ doodle.go | 26 +------- draw/line.go | 8 +-- draw/line_test.go | 8 +-- editor_scene.go | 145 ++++++++++++++++++++++++++++++-------------- level/types.go | 11 ++-- play_scene.go | 92 +++++++++++++++++++++------- render/grid.go | 49 +++++++++++++++ render/interface.go | 41 +++++++++++++ scene.go | 7 ++- shell.go | 6 ++ 13 files changed, 474 insertions(+), 106 deletions(-) create mode 100644 doodads/doodads.go create mode 100644 doodads/player.go create mode 100644 render/grid.go diff --git a/balance/shell.go b/balance/shell.go index 2be7193..c0d6494 100644 --- a/balance/shell.go +++ b/balance/shell.go @@ -8,10 +8,10 @@ var ( ShellBackgroundColor = render.Color{0, 10, 20, 128} ShellForegroundColor = render.White ShellPadding int32 = 8 - ShellFontSize = 14 + ShellFontSize = 16 ShellCursorBlinkRate uint64 = 20 ShellHistoryLineCount = 8 // Ticks that a flashed message persists for. - FlashTTL uint64 = 200 + FlashTTL uint64 = 400 ) diff --git a/doodads/doodads.go b/doodads/doodads.go new file mode 100644 index 0000000..cf673ef --- /dev/null +++ b/doodads/doodads.go @@ -0,0 +1,113 @@ +package doodads + +import ( + "git.kirsle.net/apps/doodle/render" +) + +// Doodad is a reusable drawing component used in Doodle. Doodads are buttons, +// doors, switches, the player characters themselves, anything that isn't a part +// of the level geometry. +type Doodad interface { + ID() string + + // Position and velocity, not saved to disk. + Position() render.Point + Velocity() render.Point + Size() render.Rect + + // Movement commands. + MoveBy(render.Point) // Add {X,Y} to current Position. + MoveTo(render.Point) // Set current Position to {X,Y}. + + // Implement the Draw function. + Draw(render.Engine) +} + +// Collide describes how a collision occurred. +type Collide struct { + X int32 + Y int32 + W int32 + H int32 + Top bool + Left bool + Right bool + Bottom bool +} + +// CollidesWithGrid checks if a Doodad collides with level geometry. +func CollidesWithGrid(d Doodad, grid *render.Grid) (Collide, bool) { + var ( + P = d.Position() + S = d.Size() + topLeft = P + topRight = render.Point{ + X: P.X + S.W, + Y: P.Y, + } + bottomLeft = render.Point{ + X: P.X, + Y: P.Y + S.H, + } + bottomRight = render.Point{ + X: bottomLeft.X + S.W, + Y: P.Y + S.H, + } + ) + + // Bottom edge. + for point := range render.IterLine2(bottomLeft, bottomRight) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Bottom: true, + X: point.X, + Y: point.Y, + }, true + } + } + + // Top edge. + for point := range render.IterLine2(topLeft, topRight) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Top: true, + X: point.X, + Y: point.Y, + }, true + } + } + + for point := range render.IterLine2(topLeft, bottomLeft) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Left: true, + X: point.X, + Y: point.Y, + }, true + } + } + + for point := range render.IterLine2(topRight, bottomRight) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Right: true, + X: point.X, + Y: point.Y, + }, true + } + } + + return Collide{}, false +} diff --git a/doodads/player.go b/doodads/player.go new file mode 100644 index 0000000..3f3372f --- /dev/null +++ b/doodads/player.go @@ -0,0 +1,70 @@ +package doodads + +import ( + "git.kirsle.net/apps/doodle/render" +) + +// PlayerID is the Doodad ID for the player character. +const PlayerID = "PLAYER" + +// Player is a special doodad for the player character. +type Player struct { + point render.Point + velocity render.Point + size render.Rect +} + +// NewPlayer creates the special Player Character doodad. +func NewPlayer() *Player { + return &Player{ + point: render.Point{ + X: 100, + Y: 100, + }, + size: render.Rect{ + W: 16, + H: 16, + }, + } +} + +// ID of the Player singleton. +func (p *Player) ID() string { + return PlayerID +} + +// Position of the player. +func (p *Player) Position() render.Point { + return p.point +} + +// MoveBy a relative delta position. +func (p *Player) MoveBy(by render.Point) { + p.point.X += by.X + p.point.Y += by.Y +} + +// MoveTo an absolute position. +func (p *Player) MoveTo(to render.Point) { + p.point = to +} + +// Velocity returns the player's current velocity. +func (p *Player) Velocity() render.Point { + return p.velocity +} + +// Size returns the player's size. +func (p *Player) Size() render.Rect { + return p.size +} + +// Draw the player sprite. +func (p *Player) Draw(e render.Engine) { + e.DrawRect(render.Magenta, render.Rect{ + X: p.point.X, + Y: p.point.Y, + W: p.size.W, + H: p.size.H, + }) +} diff --git a/doodle.go b/doodle.go index ac86522..79c8dda 100644 --- a/doodle.go +++ b/doodle.go @@ -1,7 +1,6 @@ package doodle import ( - "fmt" "time" "git.kirsle.net/apps/doodle/render" @@ -167,30 +166,9 @@ func (d *Doodle) EditLevel(filename string) error { // PlayLevel loads a map from JSON into the PlayScene. func (d *Doodle) PlayLevel(filename string) error { log.Info("Loading level from file: %s", filename) - scene := &PlayScene{} - err := scene.LoadLevel(filename) - if err != nil { - return err + scene := &PlayScene{ + Filename: filename, } d.Goto(scene) return nil } - -// Pixel TODO: not a global -type Pixel struct { - start bool - x int32 - y int32 - dx int32 - dy int32 -} - -func (p Pixel) String() string { - return fmt.Sprintf("(%d,%d) delta (%d,%d)", - p.x, p.y, - p.dx, p.dy, - ) -} - -// Grid is a 2D grid of pixels in X,Y notation. -type Grid map[Pixel]interface{} diff --git a/draw/line.go b/draw/line.go index 45dfab5..a174164 100644 --- a/draw/line.go +++ b/draw/line.go @@ -3,13 +3,13 @@ package draw import ( "math" - "git.kirsle.net/apps/doodle/types" + "git.kirsle.net/apps/doodle/render" ) // Line is a generator that returns the X,Y coordinates to draw a line. // https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm) -func Line(x1, y1, x2, y2 int32) chan types.Point { - generator := make(chan types.Point) +func Line(x1, y1, x2, y2 int32) chan render.Point { + generator := make(chan render.Point) go func() { var ( @@ -28,7 +28,7 @@ func Line(x1, y1, x2, y2 int32) chan types.Point { x := float64(x1) y := float64(y1) for i := 0; i <= int(step); i++ { - generator <- types.Point{ + generator <- render.Point{ X: int32(x), Y: int32(y), } diff --git a/draw/line_test.go b/draw/line_test.go index d796ae3..eb1cabb 100644 --- a/draw/line_test.go +++ b/draw/line_test.go @@ -5,7 +5,7 @@ import ( "testing" "git.kirsle.net/apps/doodle/draw" - "git.kirsle.net/apps/doodle/types" + "git.kirsle.net/apps/doodle/render" ) func TestLine(t *testing.T) { @@ -14,7 +14,7 @@ func TestLine(t *testing.T) { X2 int32 Y1 int32 Y2 int32 - Expect []types.Point + Expect []render.Point } toString := func(t task) string { return fmt.Sprintf("Line<%d,%d -> %d,%d>", @@ -29,7 +29,7 @@ func TestLine(t *testing.T) { Y1: 0, X2: 0, Y2: 10, - Expect: []types.Point{ + Expect: []render.Point{ {X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, @@ -48,7 +48,7 @@ func TestLine(t *testing.T) { Y1: 10, X2: 15, Y2: 15, - Expect: []types.Point{ + Expect: []render.Point{ {X: 10, Y: 10}, {X: 11, Y: 11}, {X: 12, Y: 12}, diff --git a/editor_scene.go b/editor_scene.go index dbed66a..b71e21c 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -16,9 +16,14 @@ import ( // EditorScene manages the "Edit Level" game mode. type EditorScene struct { + // Configuration for the scene initializer. + OpenFile bool + Filename string + Canvas render.Grid + // History of all the pixels placed by the user. - pixelHistory []Pixel - canvas Grid + pixelHistory []render.Pixel + canvas render.Grid filename string // Last saved filename. // Canvas size @@ -33,11 +38,32 @@ func (s *EditorScene) Name() string { // Setup the editor scene. func (s *EditorScene) Setup(d *Doodle) error { + // Were we given configuration data? + if s.Filename != "" { + log.Debug("EditorScene: Set filename to %s", s.Filename) + s.filename = s.Filename + s.Filename = "" + if s.OpenFile { + log.Debug("EditorScene: Loading map from filename at %s", s.filename) + if err := s.LoadLevel(s.filename); err != nil { + return err + } + } + } + if s.Canvas != nil { + log.Debug("EditorScene: Received Canvas from caller") + s.canvas = s.Canvas + s.Canvas = nil + } + + d.Flash("Editor Mode. Press 'P' to play this map.") + if s.pixelHistory == nil { - s.pixelHistory = []Pixel{} + s.pixelHistory = []render.Pixel{} } if s.canvas == nil { - s.canvas = Grid{} + log.Debug("EditorScene: Setting default canvas to an empty grid") + s.canvas = render.Grid{} } s.width = d.width // TODO: canvas width = copy the window size s.height = d.height @@ -52,31 +78,45 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { s.Screenshot() } + // Switching to Play Mode? + if ev.KeyName.Read() == "p" { + log.Info("Play Mode, Go!") + d.Goto(&PlayScene{ + Canvas: s.canvas, + }) + return nil + } + // Clear the canvas and fill it with white. d.Engine.Clear(render.White) // Clicking? Log all the pixels while doing so. if ev.Button1.Now { - pixel := Pixel{ - start: ev.Button1.Pressed(), - x: ev.CursorX.Now, - y: ev.CursorY.Now, - dx: ev.CursorX.Now, - dy: ev.CursorY.Now, + log.Warn("Button1: %+v", ev.Button1) + pixel := render.Pixel{ + Start: ev.Button1.Pressed(), + X: ev.CursorX.Now, + Y: ev.CursorY.Now, + DX: ev.CursorX.Last, + DY: ev.CursorY.Last, + } + if pixel.Start { + log.Warn("START PIXEL %+v", pixel) } // Append unique new pixels. if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel { // If not a start pixel, make the delta coord the previous one. - if !pixel.start && len(s.pixelHistory) > 0 { + if !pixel.Start && len(s.pixelHistory) > 0 { prev := s.pixelHistory[len(s.pixelHistory)-1] - pixel.dx = prev.x - pixel.dy = prev.y + pixel.DY = prev.Y + pixel.DX = prev.X } s.pixelHistory = append(s.pixelHistory, pixel) // Save in the pixel canvas map. + fmt.Printf("%+v", pixel) s.canvas[pixel] = nil } } @@ -86,24 +126,26 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { // Draw the current frame. func (s *EditorScene) Draw(d *Doodle) error { - for i, pixel := range s.pixelHistory { - if !pixel.start && i > 0 { - prev := s.pixelHistory[i-1] - if prev.x == pixel.x && prev.y == pixel.y { - d.Engine.DrawPoint( - render.Black, - render.Point{pixel.x, pixel.y}, - ) - } else { - d.Engine.DrawLine( - render.Black, - render.Point{pixel.x, pixel.y}, - render.Point{prev.x, prev.y}, - ) - } - } - d.Engine.DrawPoint(render.Black, render.Point{pixel.x, pixel.y}) - } + // for i, pixel := range s.pixelHistory { + // if !pixel.Start && i > 0 { + // prev := s.pixelHistory[i-1] + // if prev.X == pixel.X && prev.Y == pixel.Y { + // d.Engine.DrawPoint( + // render.Black, + // render.Point{pixel.X, pixel.Y}, + // ) + // } else { + // d.Engine.DrawLine( + // render.Black, + // render.Point{pixel.X, pixel.Y}, + // render.Point{prev.X, prev.Y}, + // ) + // } + // } + // d.Engine.DrawPoint(render.Black, render.Point{pixel.X, pixel.Y}) + // } + + s.canvas.Draw(d.Engine) return nil } @@ -111,8 +153,8 @@ func (s *EditorScene) Draw(d *Doodle) error { // LoadLevel loads a level from disk. func (s *EditorScene) LoadLevel(filename string) error { s.filename = filename - s.pixelHistory = []Pixel{} - s.canvas = Grid{} + s.pixelHistory = []render.Pixel{} + s.canvas = render.Grid{} m, err := level.LoadJSON(filename) if err != nil { @@ -120,12 +162,12 @@ func (s *EditorScene) LoadLevel(filename string) error { } for _, point := range m.Pixels { - pixel := Pixel{ - start: true, - x: point.X, - y: point.Y, - dx: point.X, - dy: point.Y, + pixel := render.Pixel{ + Start: true, + X: point.X, + Y: point.Y, + DX: point.X, + DY: point.Y, } s.pixelHistory = append(s.pixelHistory, pixel) s.canvas[pixel] = nil @@ -153,12 +195,20 @@ func (s *EditorScene) SaveLevel(filename string) { } for pixel := range s.canvas { - for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) { + if pixel.DX == 0 && pixel.DY == 0 { m.Pixels = append(m.Pixels, level.Pixel{ - X: point.X, - Y: point.Y, + X: pixel.X, + Y: pixel.Y, Palette: 0, }) + } else { + for point := range render.IterLine(pixel.X, pixel.Y, pixel.DX, pixel.DY) { + m.Pixels = append(m.Pixels, level.Pixel{ + X: point.X, + Y: point.Y, + Palette: 0, + }) + } } } @@ -189,10 +239,10 @@ func (s *EditorScene) Screenshot() { // Fill in the dots we drew. for pixel := range s.canvas { // A line or a dot? - if pixel.x == pixel.dx && pixel.y == pixel.dy { - screenshot.Set(int(pixel.x), int(pixel.y), image.Black) + if pixel.DX == 0 && pixel.DY == 0 { + screenshot.Set(int(pixel.X), int(pixel.Y), image.Black) } else { - for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) { + for point := range draw.Line(pixel.X, pixel.Y, pixel.DX, pixel.DY) { screenshot.Set(int(point.X), int(point.Y), image.Black) } } @@ -223,3 +273,8 @@ func (s *EditorScene) Screenshot() { return } } + +// Destroy the scene. +func (s *EditorScene) Destroy() error { + return nil +} diff --git a/level/types.go b/level/types.go index 3709e79..9bfc1e8 100644 --- a/level/types.go +++ b/level/types.go @@ -7,11 +7,12 @@ import ( // Level is the container format for Doodle map drawings. type Level struct { - Version int32 `json:"version"` // File format version spec. - Title string `json:"title"` - Author string `json:"author"` - Password string `json:"passwd"` - Locked bool `json:"locked"` + Version int32 `json:"version"` // File format version spec. + GameVersion string `json:"gameVersion"` // Game version that created the level. + Title string `json:"title"` + Author string `json:"author"` + Password string `json:"passwd"` + Locked bool `json:"locked"` // Level size. Width int32 `json:"w"` diff --git a/play_scene.go b/play_scene.go index 37aeee4..ce1ac55 100644 --- a/play_scene.go +++ b/play_scene.go @@ -1,6 +1,7 @@ package doodle import ( + "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" @@ -8,17 +9,19 @@ import ( // PlayScene manages the "Edit Level" game mode. type PlayScene struct { - canvas Grid + // Configuration attributes. + Filename string + Canvas render.Grid + + // Private variables. + canvas render.Grid // Canvas size width int32 height int32 - // Player position and velocity. - x int32 - y int32 - vx int32 - vy int32 + // Player character + player doodads.Doodad } // Name of the scene. @@ -28,19 +31,43 @@ func (s *PlayScene) Name() string { // Setup the play scene. func (s *PlayScene) Setup(d *Doodle) error { - s.x = 10 - s.y = 10 + // Given a filename or map data to play? + if s.Canvas != nil { + log.Debug("PlayScene.Setup: loading map from given canvas") + s.canvas = s.Canvas + + } else if s.Filename != "" { + log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) + s.LoadLevel(s.Filename) + s.Filename = "" + } + + s.player = doodads.NewPlayer() if s.canvas == nil { - s.canvas = Grid{} + log.Debug("PlayScene.Setup: no grid given, initializing empty grid") + s.canvas = render.Grid{} } + s.width = d.width // TODO: canvas width = copy the window size s.height = d.height + + d.Flash("Entered Play Mode. Press 'E' to edit this map.") + return nil } // Loop the editor scene. func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { + // Switching to Edit Mode? + if ev.KeyName.Read() == "e" { + log.Info("Edit Mode, Go!") + d.Goto(&EditorScene{ + Canvas: s.canvas, + }) + return nil + } + s.movePlayer(ev) return nil } @@ -50,35 +77,53 @@ func (s *PlayScene) Draw(d *Doodle) error { // Clear the canvas and fill it with white. d.Engine.Clear(render.White) - for pixel := range s.canvas { - d.Engine.DrawPoint(render.Black, render.Point{pixel.x, pixel.y}) - } + s.canvas.Draw(d.Engine) // Draw our hero. - d.Engine.DrawRect(render.Magenta, render.Rect{s.x, s.y, 16, 16}) + s.player.Draw(d.Engine) return nil } // movePlayer updates the player's X,Y coordinate based on key pressed. func (s *PlayScene) movePlayer(ev *events.State) { + delta := s.player.Position() + var playerSpeed int32 = 8 + var gravity int32 = 2 + if ev.Down.Now { - s.y += 4 + delta.Y += playerSpeed } if ev.Left.Now { - s.x -= 4 + delta.X -= playerSpeed } if ev.Right.Now { - s.x += 4 + delta.X += playerSpeed } if ev.Up.Now { - s.y -= 4 + delta.Y -= playerSpeed } + + // Apply gravity. + delta.Y += gravity + + // Draw a ray and check for collision. + var lastOk = s.player.Position() + for point := range render.IterLine2(s.player.Position(), delta) { + s.player.MoveTo(point) + if _, ok := doodads.CollidesWithGrid(s.player, &s.canvas); ok { + s.player.MoveTo(lastOk) + } else { + lastOk = s.player.Position() + } + } + + s.player.MoveTo(lastOk) } // LoadLevel loads a level from disk. func (s *PlayScene) LoadLevel(filename string) error { - s.canvas = Grid{} + s.canvas = render.Grid{} m, err := level.LoadJSON(filename) if err != nil { @@ -86,12 +131,17 @@ func (s *PlayScene) LoadLevel(filename string) error { } for _, point := range m.Pixels { - pixel := Pixel{ - x: point.X, - y: point.Y, + pixel := render.Pixel{ + X: point.X, + Y: point.Y, } s.canvas[pixel] = nil } return nil } + +// Destroy the scene. +func (s *PlayScene) Destroy() error { + return nil +} diff --git a/render/grid.go b/render/grid.go new file mode 100644 index 0000000..ad80a35 --- /dev/null +++ b/render/grid.go @@ -0,0 +1,49 @@ +package render + +import ( + "fmt" +) + +// Pixel TODO: not a global +// TODO get rid of this ugly thing. +type Pixel struct { + Start bool + X int32 + Y int32 + DX int32 + DY int32 +} + +func (p Pixel) String() string { + return fmt.Sprintf("(%d,%d) delta (%d,%d)", + p.X, p.Y, + p.DX, p.DY, + ) +} + +// Grid is a 2D grid of pixels in X,Y notation. +type Grid map[Pixel]interface{} + +// Exists returns true if the point exists on the grid. +func (g *Grid) Exists(p Pixel) bool { + if _, ok := (*g)[p]; ok { + return true + } + return false +} + +// Draw the grid efficiently. +func (g *Grid) Draw(e Engine) { + for pixel := range *g { + if pixel.DX == 0 && pixel.DY == 0 { + e.DrawPoint(Black, Point{ + X: pixel.X, + Y: pixel.Y, + }) + } else { + for point := range IterLine(pixel.X, pixel.Y, pixel.DX, pixel.DY) { + e.DrawPoint(Black, point) + } + } + } +} diff --git a/render/interface.go b/render/interface.go index 8a0e9aa..b4713d1 100644 --- a/render/interface.go +++ b/render/interface.go @@ -2,6 +2,7 @@ package render import ( "fmt" + "math" "git.kirsle.net/apps/doodle/events" ) @@ -103,3 +104,43 @@ var ( Magenta = Color{255, 0, 255, 255} Pink = Color{255, 153, 255, 255} ) + +// IterLine is a generator that returns the X,Y coordinates to draw a line. +// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm) +func IterLine(x1, y1, x2, y2 int32) chan Point { + generator := make(chan Point) + + go func() { + var ( + dx = float64(x2 - x1) + dy = float64(y2 - y1) + ) + var step float64 + if math.Abs(dx) >= math.Abs(dy) { + step = math.Abs(dx) + } else { + step = math.Abs(dy) + } + + dx = dx / step + dy = dy / step + x := float64(x1) + y := float64(y1) + for i := 0; i <= int(step); i++ { + generator <- Point{ + X: int32(x), + Y: int32(y), + } + x += dx + y += dy + } + + close(generator) + }() + + return generator +} + +func IterLine2(p1 Point, p2 Point) chan Point { + return IterLine(p1.X, p1.Y, p2.X, p2.Y) +} diff --git a/scene.go b/scene.go index 26e6865..f7d4e78 100644 --- a/scene.go +++ b/scene.go @@ -8,6 +8,7 @@ import "git.kirsle.net/apps/doodle/events" type Scene interface { Name() string Setup(*Doodle) error + Destroy() error // Loop should update the scene's state but not draw anything. Loop(*Doodle, *events.State) error @@ -19,7 +20,11 @@ type Scene interface { // Goto a scene. First it unloads the current scene. func (d *Doodle) Goto(scene Scene) error { - // d.scene.Destroy() + // Teardown existing scene. + if d.scene != nil { + d.scene.Destroy() + } + log.Info("Goto Scene") d.scene = scene return d.scene.Setup(d) diff --git a/shell.go b/shell.go index 0d5d74b..10418de 100644 --- a/shell.go +++ b/shell.go @@ -2,6 +2,7 @@ package doodle import ( "bytes" + "fmt" "strings" "git.kirsle.net/apps/doodle/balance" @@ -9,6 +10,11 @@ import ( "git.kirsle.net/apps/doodle/render" ) +// Flash a message to the user. +func (d *Doodle) Flash(template string, v ...interface{}) { + d.shell.Write(fmt.Sprintf(template, v...)) +} + // Shell implements the developer console in-game. type Shell struct { parent *Doodle