Fix Play Mode, Level Handover & Collision Detection

* Edit Mode now uses the Level object itself to keep the drawing data
  rather than pull its Palette and Chunks out, so it can hang on to more
  information. The Canvas widget is given references to the
  Level.Palette and Level.Chunker via Canvas.LoadLevel()
* Fix the handoff between Edit Mode and Play Mode. They pass the Level
  object back and forth and the Filename, because it's not part of the
  Level. You can save the map with its original settings after returning
  from Play Mode.
* Fix the collision detection in Play Mode. It broke previously when
  palettes were added because of the difference between a render.Point
  and a level.Pixel and it couldn't easily look up coordinates. The new
  Chunker system provides a render.Point lookup API.
* All pixels are solid for collision right now, TODO is to return Swatch
  information from the pixels touching the player character and react
  accordingly (non-solid, fire flag, etc.)
* Remove the level.Grid type as it has been replaced by the Chunker.
* Clean up some unused variables and functions.
This commit is contained in:
Noah 2018-09-25 09:40:34 -07:00
parent 3c185528f9
commit e25869644c
10 changed files with 117 additions and 115 deletions

View File

@ -30,12 +30,16 @@ type Doodad interface {
type Collide struct { type Collide struct {
Top bool Top bool
TopPoint render.Point TopPoint render.Point
TopPixel *level.Swatch
Left bool Left bool
LeftPoint render.Point LeftPoint render.Point
LeftPixel *level.Swatch
Right bool Right bool
RightPoint render.Point RightPoint render.Point
RightPixel *level.Swatch
Bottom bool Bottom bool
BottomPoint render.Point BottomPoint render.Point
BottomPixel *level.Swatch
MoveTo render.Point MoveTo render.Point
} }
@ -68,7 +72,7 @@ const (
) )
// CollidesWithGrid checks if a Doodad collides with level geometry. // CollidesWithGrid checks if a Doodad collides with level geometry.
func CollidesWithGrid(d Doodad, grid *level.Grid, target render.Point) (*Collide, bool) { func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Collide, bool) {
var ( var (
P = d.Position() P = d.Position()
S = d.Size() S = d.Size()
@ -280,7 +284,7 @@ func GetCollisionBox(box render.Rect) CollisionBox {
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and // ScanBoundingBox scans all of the pixels in a bounding box on the grid and
// returns if any of them intersect with level geometry. // returns if any of them intersect with level geometry.
func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Grid) bool { func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
col := GetCollisionBox(box) col := GetCollisionBox(box)
c.ScanGridLine(col.Top[0], col.Top[1], grid, Top) c.ScanGridLine(col.Top[0], col.Top[1], grid, Top)
@ -293,12 +297,9 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Grid) bool {
// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests // ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests
// for any pixels to be set, implying a collision between level geometry and the // for any pixels to be set, implying a collision between level geometry and the
// bounding boxes of the doodad. // bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Grid, side Side) { func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
for point := range render.IterLine2(p1, p2) { for point := range render.IterLine2(p1, p2) {
if grid.Exists(&level.Pixel{ if _, err := grid.Get(point); err == nil {
X: point.X,
Y: point.Y,
}) {
// A hit! // A hit!
switch side { switch side {
case Top: case Top:

View File

@ -1,10 +1,12 @@
package doodle package doodle
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
@ -15,20 +17,19 @@ type EditorScene struct {
// Configuration for the scene initializer. // Configuration for the scene initializer.
OpenFile bool OpenFile bool
Filename string Filename string
Canvas *level.Chunker
UI *EditorUI UI *EditorUI
// The current level being edited.
DrawingType enum.DrawingType
Level *level.Level
// The canvas widget that contains the map we're working on. // The canvas widget that contains the map we're working on.
// XXX: in dev builds this is available at $ d.Scene.GetDrawing() // XXX: in dev builds this is available at $ d.Scene.GetDrawing()
drawing *level.Canvas drawing *level.Canvas
// History of all the pixels placed by the user. // Last saved filename by the user.
filename string // Last saved filename. filename string
// Canvas size
width int32
height int32
} }
// Name of the scene. // Name of the scene.
@ -39,27 +40,37 @@ func (s *EditorScene) Name() string {
// Setup the editor scene. // Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error { func (s *EditorScene) Setup(d *Doodle) error {
s.drawing = level.NewCanvas(balance.ChunkSize, true) s.drawing = level.NewCanvas(balance.ChunkSize, true)
s.drawing.Palette = level.DefaultPalette()
if len(s.drawing.Palette.Swatches) > 0 { if len(s.drawing.Palette.Swatches) > 0 {
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0]) s.drawing.SetSwatch(s.drawing.Palette.Swatches[0])
} }
// Were we given configuration data? // TODO: move inside the UI. Just an approximate position for now.
s.drawing.MoveTo(render.NewPoint(0, 19))
s.drawing.Resize(render.NewRect(d.width-150, d.height-44))
s.drawing.Compute(d.Engine)
// // Were we given configuration data?
if s.Filename != "" { if s.Filename != "" {
log.Debug("EditorScene: Set filename to %s", s.Filename) log.Debug("EditorScene.Setup: Set filename to %s", s.Filename)
s.filename = s.Filename s.filename = s.Filename
s.Filename = "" s.Filename = ""
if s.OpenFile { }
log.Debug("EditorScene: Loading map from filename at %s", s.filename) if s.Level != nil {
if err := s.LoadLevel(s.filename); err != nil { log.Debug("EditorScene.Setup: received level from scene caller")
d.Flash("LoadLevel error: %s", err) s.drawing.LoadLevel(s.Level)
} } else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
if err := s.LoadLevel(s.filename); err != nil {
d.Flash("LoadLevel error: %s", err)
} }
} }
if s.Canvas != nil {
log.Debug("EditorScene: Received Canvas from caller") // No level?
s.drawing.Load(s.drawing.Palette, s.Canvas) if s.Level == nil {
s.Canvas = nil log.Debug("EditorScene.Setup: initializing a new Level")
s.Level = level.New()
s.Level.Palette = level.DefaultPalette()
s.drawing.LoadLevel(s.Level)
} }
// Initialize the user interface. It references the palette and such so it // Initialize the user interface. It references the palette and such so it
@ -67,8 +78,6 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.UI = NewEditorUI(d, s) s.UI = NewEditorUI(d, s)
d.Flash("Editor Mode. Press 'P' to play this map.") d.Flash("Editor Mode. Press 'P' to play this map.")
s.width = d.width // TODO: canvas width = copy the window size
s.height = d.height
return nil return nil
} }
@ -81,7 +90,8 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
if ev.KeyName.Read() == "p" { if ev.KeyName.Read() == "p" {
log.Info("Play Mode, Go!") log.Info("Play Mode, Go!")
d.Goto(&PlayScene{ d.Goto(&PlayScene{
// Canvas: s.drawing.Grid(), XXX Filename: s.filename,
Level: s.Level,
}) })
return nil return nil
} }
@ -95,11 +105,6 @@ func (s *EditorScene) Draw(d *Doodle) error {
d.Engine.Clear(render.Magenta) d.Engine.Clear(render.Magenta)
s.UI.Present(d.Engine) s.UI.Present(d.Engine)
// TODO: move inside the UI. Just an approximate position for now.
s.drawing.MoveTo(render.NewPoint(0, 19))
s.drawing.Resize(render.NewRect(d.width-150, d.height-44))
s.drawing.Compute(d.Engine)
s.drawing.Present(d.Engine, s.drawing.Point()) s.drawing.Present(d.Engine, s.drawing.Point())
return nil return nil
@ -108,8 +113,16 @@ func (s *EditorScene) Draw(d *Doodle) error {
// LoadLevel loads a level from disk. // LoadLevel loads a level from disk.
func (s *EditorScene) LoadLevel(filename string) error { func (s *EditorScene) LoadLevel(filename string) error {
s.filename = filename s.filename = filename
return s.drawing.LoadFilename(filename)
level, err := level.LoadJSON(filename)
if err != nil {
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
}
s.DrawingType = enum.LevelDrawing
s.Level = level
s.drawing.LoadLevel(s.Level)
return nil
} }
// SaveLevel saves the level to disk. // SaveLevel saves the level to disk.
@ -117,11 +130,14 @@ func (s *EditorScene) LoadLevel(filename string) error {
func (s *EditorScene) SaveLevel(filename string) { func (s *EditorScene) SaveLevel(filename string) {
s.filename = filename s.filename = filename
m := level.New() m := s.Level
m.Title = "Alpha" if m.Title == "" {
m.Author = os.Getenv("USER") m.Title = "Alpha"
m.Width = s.width }
m.Height = s.height if m.Author == "" {
m.Author = os.Getenv("USER")
}
m.Palette = s.drawing.Palette m.Palette = s.drawing.Palette
m.Chunker = s.drawing.Chunker() m.Chunker = s.drawing.Chunker()

12
enum/enum.go Normal file
View File

@ -0,0 +1,12 @@
// Package enum defines all the little enum types used throughout Doodle.
package enum
// DrawingType tells the EditorScene whether the currently open drawing is
// a Level or a Doodad.
type DrawingType int
// EditorType values.
const (
LevelDrawing DrawingType = iota
DoodadDrawing
)

View File

@ -38,23 +38,15 @@ func NewCanvas(size int, editable bool) *Canvas {
func (w *Canvas) Load(p *Palette, g *Chunker) { func (w *Canvas) Load(p *Palette, g *Chunker) {
w.Palette = p w.Palette = p
w.chunks = g w.chunks = g
}
// LoadFilename initializes the Canvas using a file on disk.
func (w *Canvas) LoadFilename(filename string) error {
m, err := LoadJSON(filename)
if err != nil {
return err
}
w.Palette = m.Palette
w.chunks = m.Chunker
if len(w.Palette.Swatches) > 0 { if len(w.Palette.Swatches) > 0 {
w.SetSwatch(w.Palette.Swatches[0]) w.SetSwatch(w.Palette.Swatches[0])
} }
}
return nil // LoadLevel initializes a Canvas from a Level object.
func (w *Canvas) LoadLevel(level *Level) {
w.Load(level.Palette, level.Chunker)
} }
// SetSwatch changes the currently selected swatch for editing. // SetSwatch changes the currently selected swatch for editing.
@ -117,10 +109,9 @@ func (w *Canvas) Loop(ev *events.State) error {
Y: ev.CursorY.Now - P.Y + w.Scroll.Y, Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
} }
pixel := &Pixel{ pixel := &Pixel{
X: cursor.X, X: cursor.X,
Y: cursor.Y, Y: cursor.Y,
Palette: w.Palette, Swatch: w.Palette.ActiveSwatch,
Swatch: w.Palette.ActiveSwatch,
} }
// Append unique new pixels. // Append unique new pixels.

View File

@ -29,7 +29,7 @@ func NewChunker(size int) *Chunker {
// on disk) to connect references to the swatches in the palette. // on disk) to connect references to the swatches in the palette.
func (c *Chunker) Inflate(pal *Palette) error { func (c *Chunker) Inflate(pal *Palette) error {
for coord, chunk := range c.Chunks { for coord, chunk := range c.Chunks {
log.Debug("Chunker.Inflate: expanding chunk %s %+v", coord, chunk) log.Debug("Chunker.Inflate: expanding chunk %s", coord)
chunk.Inflate(pal) chunk.Inflate(pal)
} }
return nil return nil
@ -52,7 +52,11 @@ func (c *Chunker) IterViewport(viewport render.Rect) <-chan Pixel {
for cy := topLeft.Y; cy <= bottomRight.Y; cy++ { for cy := topLeft.Y; cy <= bottomRight.Y; cy++ {
if chunk, ok := c.GetChunk(render.NewPoint(cx, cy)); ok { if chunk, ok := c.GetChunk(render.NewPoint(cx, cy)); ok {
for px := range chunk.Iter() { for px := range chunk.Iter() {
pipe <- px
// Verify this pixel is also in range.
if px.Point().Inside(viewport) {
pipe <- px
}
} }
} }
} }

View File

@ -1,27 +0,0 @@
package level
import (
"git.kirsle.net/apps/doodle/render"
)
// 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 render.Engine) {
for pixel := range *g {
color := pixel.Swatch.Color
e.DrawPoint(color, render.Point{
X: pixel.X,
Y: pixel.Y,
})
}
}

View File

@ -57,7 +57,6 @@ func LoadJSON(filename string) (*Level, error) {
px, px.PaletteIndex, len(m.Palette.Swatches), px, px.PaletteIndex, len(m.Palette.Swatches),
) )
} }
px.Palette = m.Palette
px.Swatch = m.Palette.Swatches[px.PaletteIndex] px.Swatch = m.Palette.Swatches[px.PaletteIndex]
} }
return m, err return m, err

View File

@ -50,8 +50,7 @@ type Pixel struct {
PaletteIndex int32 `json:"p"` PaletteIndex int32 `json:"p"`
// Private runtime values. // Private runtime values.
Palette *Palette `json:"-"` // pointer to its palette, TODO: needed? Swatch *Swatch `json:"-"` // pointer to its swatch, for when rendered.
Swatch *Swatch `json:"-"` // pointer to its swatch, for when rendered.
} }
func (p Pixel) String() string { func (p Pixel) String() string {

View File

@ -1,6 +1,9 @@
package doodle package doodle
import ( import (
"fmt"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
@ -11,14 +14,10 @@ import (
type PlayScene struct { type PlayScene struct {
// Configuration attributes. // Configuration attributes.
Filename string Filename string
Canvas *level.Grid Level *level.Level
// Private variables. // Private variables.
canvas *level.Grid drawing *level.Canvas
// Canvas size
width int32
height int32
// Player character // Player character
Player doodads.Doodad Player doodads.Doodad
@ -31,27 +30,28 @@ func (s *PlayScene) Name() string {
// Setup the play scene. // Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error { func (s *PlayScene) Setup(d *Doodle) error {
// Given a filename or map data to play? s.drawing = level.NewCanvas(balance.ChunkSize, false)
if s.Canvas != nil { s.drawing.MoveTo(render.Origin)
log.Debug("PlayScene.Setup: loading map from given canvas") s.drawing.Resize(render.NewRect(d.width, d.height))
s.canvas = s.Canvas s.drawing.Compute(d.Engine)
// Given a filename or map data to play?
if s.Level != nil {
log.Debug("PlayScene.Setup: received level from scene caller")
s.drawing.LoadLevel(s.Level)
} else if s.Filename != "" { } else if s.Filename != "" {
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
s.LoadLevel(s.Filename) s.LoadLevel(s.Filename)
s.Filename = ""
} }
s.Player = doodads.NewPlayer() s.Player = doodads.NewPlayer()
if s.canvas == nil { if s.Level == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid") log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
s.canvas = &level.Grid{} s.Level = level.New()
s.drawing.LoadLevel(s.Level)
} }
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.") d.Flash("Entered Play Mode. Press 'E' to edit this map.")
return nil return nil
@ -63,11 +63,13 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
if ev.KeyName.Read() == "e" { if ev.KeyName.Read() == "e" {
log.Info("Edit Mode, Go!") log.Info("Edit Mode, Go!")
d.Goto(&EditorScene{ d.Goto(&EditorScene{
// Canvas: s.canvas, Filename: s.Filename,
Level: s.Level,
}) })
return nil return nil
} }
// s.drawing.Loop(ev)
s.movePlayer(ev) s.movePlayer(ev)
return nil return nil
} }
@ -77,7 +79,8 @@ func (s *PlayScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white. // Clear the canvas and fill it with white.
d.Engine.Clear(render.White) d.Engine.Clear(render.White)
s.canvas.Draw(d.Engine) // Draw the level.
s.drawing.Present(d.Engine, s.drawing.Point())
// Draw our hero. // Draw our hero.
s.Player.Draw(d.Engine) s.Player.Draw(d.Engine)
@ -110,7 +113,7 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// Apply gravity. // Apply gravity.
// var onFloor bool // var onFloor bool
info, ok := doodads.CollidesWithGrid(s.Player, s.canvas, delta) info, ok := doodads.CollidesWithGrid(s.Player, s.Level.Chunker, delta)
if ok { if ok {
// Collision happened with world. // Collision happened with world.
} }
@ -128,16 +131,15 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// LoadLevel loads a level from disk. // LoadLevel loads a level from disk.
func (s *PlayScene) LoadLevel(filename string) error { func (s *PlayScene) LoadLevel(filename string) error {
s.canvas = &level.Grid{} s.Filename = filename
// m, err := level.LoadJSON(filename) level, err := level.LoadJSON(filename)
// if err != nil { if err != nil {
// return err return fmt.Errorf("PlayScene.LoadLevel(%s): %s", filename, err)
// } }
// for _, pixel := range m.Pixels { s.Level = level
// // *s.canvas[pixel] = nil s.drawing.LoadLevel(s.Level)
// }
return nil return nil
} }

View File

@ -12,6 +12,11 @@ type Point struct {
Y int32 Y int32
} }
// Common points.
var (
Origin Point
)
// NewPoint makes a new Point at an X,Y coordinate. // NewPoint makes a new Point at an X,Y coordinate.
func NewPoint(x, y int32) Point { func NewPoint(x, y int32) Point {
return Point{ return Point{