Files
ordoor/cmd/view-map/main.go
Nick Thomas 5fccf97f4b Lazily load sprite image data
This cuts memory use significantly, since many sprites in an object are
never used. We can get savings over time by evicting sprites when they
go out of scope, but that's, well, out of scope.

To achieve this, I introduce an assetstore package that is in charge of
loading things from the filesystem. This also allows some lingering
case-sensitivity issues to be handled cleanly.

I'd hoped that creating fewer ebiten.Image instances would help CPU
usage, but that doesn't seem to be the case.
2020-03-19 22:24:21 +00:00

200 lines
4.3 KiB
Go

package main
import (
"flag"
"image"
"log"
"math"
"os"
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/ui"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
)
type env struct {
assets *assetstore.AssetStore
area *assetstore.Map
step int
state state
lastState state
}
type state struct {
zoom float64
origin image.Point
zIdx int
}
func main() {
flag.Parse()
if *gamePath == "" || *gameMap == "" {
flag.Usage()
os.Exit(1)
}
assets, err := assetstore.New(*gamePath)
if err != nil {
log.Fatalf("Failed to scan root directory %v: %v", *gamePath, err)
}
area, err := assets.Map(*gameMap)
if err != nil {
log.Fatalf("Failed to load map %v: %v", *gameMap, err)
}
state := state{
zoom: 1.0,
origin: image.Point{0, 3000}, // FIXME: haxxx
}
env := &env{
area: area,
assets: assets,
state: state,
lastState: state,
}
win, err := ui.NewWindow("View Map " + *gameMap)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
}
// TODO: click to view cell data
win.OnKeyUp(ebiten.KeyLeft, env.changeOrigin(-64, +0))
win.OnKeyUp(ebiten.KeyRight, env.changeOrigin(+64, +0))
win.OnKeyUp(ebiten.KeyUp, env.changeOrigin(+0, -64))
win.OnKeyUp(ebiten.KeyDown, env.changeOrigin(+0, +64))
win.OnMouseWheel(env.changeZoom)
for i := 0; i <= 6; i++ {
win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i))
}
if err := win.Run(env.Update, env.Draw); err != nil {
log.Fatal(err)
}
}
func (e *env) Update() error {
if e.step == 0 || e.lastState != e.state {
log.Printf("zoom=%.2f zIdx=%v camPos=%#v", e.state.zoom, e.state.zIdx, e.state.origin)
}
e.lastState = e.state
e.step += 1
return nil
}
func (e *env) Draw(screen *ebiten.Image) error {
// Bounds clipping
// http://www.java-gaming.org/index.php?topic=24922.0
// https://stackoverflow.com/questions/892811/drawing-isometric-game-worlds
// https://gamedev.stackexchange.com/questions/25896/how-do-i-find-which-isometric-tiles-are-inside-the-cameras-current-view
sw, sh := screen.Size()
topLeftX, topLeftY := pixToCell(
float64(e.state.origin.X),
float64(e.state.origin.Y),
)
topLeftX -= 1 // Otherwise we miss half a cell on alternate rows on the left
bottomRightX, bottomRightY := pixToCell(
float64(e.state.origin.X+sw),
float64(e.state.origin.Y+sh),
)
// X+Y is constant for all tiles in a column
// X-Y is constant for all tiles in a row
for a := int(topLeftX + topLeftY); a <= int(bottomRightX+bottomRightY); a++ {
for b := int(topLeftX - topLeftY); b <= int(bottomRightX-bottomRightY); b++ {
if b&1 != a&1 {
continue
}
x := (a + b) / 2
y := (a - b) / 2
if !image.Pt(x, y).In(e.area.Rect) {
continue
}
for z := 0; z <= e.state.zIdx; z++ {
e.renderCell(x, y, z, screen)
}
}
}
return nil
}
func (e *env) renderCell(x, y, z int, screen *ebiten.Image) error {
images, err := e.area.ImagesForCell(x, y, z)
if err != nil {
return err
}
iso := ebiten.GeoM{}
iso.Translate(-float64(e.state.origin.X), -float64(e.state.origin.Y))
fx, fy := cellToPix(float64(x), float64(y))
iso.Translate(fx, fy)
// Taking the Z index away *seems* to draw the object in the correct place.
// FIXME: There are some artifacts, investigate more
iso.Translate(0.0, -float64(z*48.0)) // offset for Z index
// TODO: iso.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
for _, img := range images {
if err := screen.DrawImage(img, &ebiten.DrawImageOptions{GeoM: iso}); err != nil {
return err
}
}
return nil
}
func (e *env) changeOrigin(byX, byY int) func() {
return func() {
e.state.origin.X += byX
e.state.origin.Y += byY
}
}
func (e *env) changeZoom(_, y float64) {
// Zoom in and out with the mouse wheel
e.state.zoom *= math.Pow(1.2, y)
}
func (e *env) setZIdx(to int) func() {
return func() {
e.state.zIdx = to
}
}
const (
cellWidth = 64
cellHeight = 64
)
// Doesn't take the camera or Z level into account
func cellToPix(x, y float64) (float64, float64) {
return (x - y) * cellWidth, (x + y) * cellHeight / 2.0
}
// Doesn't take the camera or Z level into account
func pixToCell(x, y float64) (float64, float64) {
return y/cellHeight + x/(cellWidth*2.0), y/cellHeight - x/(cellWidth*2.0)
}