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.
chunks
Noah 2018-07-23 20:10:53 -07:00
parent e13dd62309
commit e141203c4b
13 changed files with 474 additions and 106 deletions

View File

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

113
doodads/doodads.go Normal file
View File

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

70
doodads/player.go Normal file
View File

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

View File

@ -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{}

View File

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

View File

@ -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},

View File

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

View File

@ -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"`

View File

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

49
render/grid.go Normal file
View File

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

View File

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

View File

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

View File

@ -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