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.
200 lines
4.3 KiB
Go
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)
|
|
}
|