Add isometric demo (#1823)

Based on a prototype by Justin Cichra (@jrcichra).

Closes #1112
This commit is contained in:
Trevor Slocum 2021-09-30 19:10:48 -07:00 committed by GitHub
parent 0b4bfd471f
commit 1019e15ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 482 additions and 0 deletions

224
examples/isometric/game.go Normal file
View File

@ -0,0 +1,224 @@
// Copyright 2021 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build example
// +build example
package main
import (
"fmt"
"image"
"math"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
var spinner = []byte(`-\|/`)
// Game is an isometric demo game.
type Game struct {
w, h int
currentLevel *Level
camX, camY float64
camScale float64
camScaleTo float64
mousePanX, mousePanY int
spinnerIndex int
}
// NewGame returns a new isometric demo Game.
func NewGame() (*Game, error) {
l, err := NewLevel()
if err != nil {
return nil, fmt.Errorf("failed to create new level: %s", err)
}
g := &Game{
currentLevel: l,
camScale: 2,
camScaleTo: 2,
mousePanX: math.MinInt32,
mousePanY: math.MinInt32,
}
return g, nil
}
// Update reads current user input and updates the Game state.
func (g *Game) Update() error {
// Update target zoom level.
var scrollY float64
if ebiten.IsKeyPressed(ebiten.KeyC) || ebiten.IsKeyPressed(ebiten.KeyPageDown) {
scrollY = -0.25
} else if ebiten.IsKeyPressed(ebiten.KeyE) || ebiten.IsKeyPressed(ebiten.KeyPageUp) {
scrollY = .25
} else {
_, scrollY = ebiten.Wheel()
if scrollY < -1 {
scrollY = -1
} else if scrollY > 1 {
scrollY = 1
}
}
g.camScaleTo += scrollY * (g.camScaleTo / 7)
// Clamp target zoom level.
if g.camScaleTo < 0.01 {
g.camScaleTo = 0.01
} else if g.camScaleTo > 100 {
g.camScaleTo = 100
}
// Smooth zoom transition.
div := 10.0
if g.camScaleTo > g.camScale {
g.camScale += (g.camScaleTo - g.camScale) / div
} else if g.camScaleTo < g.camScale {
g.camScale -= (g.camScale - g.camScaleTo) / div
}
// Pan camera via keyboard.
pan := 7.0 / g.camScale
if ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA) {
g.camX -= pan
}
if ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD) {
g.camX += pan
}
if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) {
g.camY -= pan
}
if ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW) {
g.camY += pan
}
// Pan camera via mouse.
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
if g.mousePanX == math.MinInt32 && g.mousePanY == math.MinInt32 {
g.mousePanX, g.mousePanY = ebiten.CursorPosition()
} else {
x, y := ebiten.CursorPosition()
dx, dy := float64(g.mousePanX-x)*(pan/100), float64(g.mousePanY-y)*(pan/100)
g.camX, g.camY = g.camX-dx, g.camY+dy
}
} else if g.mousePanX != math.MinInt32 || g.mousePanY != math.MinInt32 {
g.mousePanX, g.mousePanY = math.MinInt32, math.MinInt32
}
// Clamp camera position.
worldWidth := float64(g.currentLevel.w * g.currentLevel.tileSize / 2)
worldHeight := float64(g.currentLevel.h * g.currentLevel.tileSize / 2)
if g.camX < worldWidth*-1 {
g.camX = worldWidth * -1
} else if g.camX > worldWidth {
g.camX = worldWidth
}
if g.camY < worldHeight*-1 {
g.camY = worldHeight * -1
} else if g.camY > 0 {
g.camY = 0
}
// Randomize level.
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
l, err := NewLevel()
if err != nil {
return fmt.Errorf("failed to create new level: %s", err)
}
g.currentLevel = l
}
return nil
}
// Draw draws the Game on the screen.
func (g *Game) Draw(screen *ebiten.Image) {
// Render level.
g.renderLevel(screen)
// Print game info.
debugBox := image.NewRGBA(image.Rect(0, 0, g.w, 200))
debugImg := ebiten.NewImageFromImage(debugBox)
ebitenutil.DebugPrint(debugImg, fmt.Sprintf("KEYS WASD EC R\nFPS %0.0f\nTPS %0.0f\nSCA %0.2f\nPOS %0.0f,%0.0f", ebiten.CurrentFPS(), ebiten.CurrentTPS(), g.camScale, g.camX, g.camY))
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(3, 0)
op.GeoM.Scale(2, 2)
screen.DrawImage(debugImg, op)
}
// Layout is called when the Game's layout changes.
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
s := ebiten.DeviceScaleFactor()
g.w, g.h = int(s*float64(outsideWidth)), int(s*float64(outsideHeight))
return g.w, g.h
}
// cartesianToIso transforms cartesian coordinates into isometric coordinates.
func (g *Game) cartesianToIso(x, y float64) (float64, float64) {
tileSize := g.currentLevel.tileSize
ix := (x - y) * float64(tileSize/2)
iy := (x + y) * float64(tileSize/4)
return ix, iy
}
/*
// isoToCartesian transforms isometric coordinates into cartesian coordinates.
func (g *Game) isoToCartesian(x, y float64) (float64, float64) {
tileSize := g.currentLevel.tileSize
cx := (x/float64(tileSize/2) + y/float64(tileSize/4)) / 2
cy := (y/float64(tileSize/4) - (x / float64(tileSize/2))) / 2
return cx, cy
}
*/
// renderLevel draws the current Level on the screen.
func (g *Game) renderLevel(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
var t *Tile
for y := 0; y < g.currentLevel.h; y++ {
for x := 0; x < g.currentLevel.w; x++ {
xi, yi := g.cartesianToIso(float64(x), float64(y))
// Skip drawing off-screen tiles.
padding := float64(g.currentLevel.tileSize) * g.camScale
drawX, drawY := ((xi-g.camX)*g.camScale)+float64(g.w/2.0), ((yi+g.camY)*g.camScale)+float64(g.h/2.0)
if drawX+padding < 0 || drawY+padding < 0 || drawX > float64(g.w) || drawY > float64(g.h) {
continue
}
t = g.currentLevel.tiles[y][x]
if t == nil {
continue // No tile at this position.
}
op.GeoM.Reset()
// Move to current isometric position.
op.GeoM.Translate(xi, yi)
// Translate camera position.
op.GeoM.Translate(-g.camX, g.camY)
// Zoom.
op.GeoM.Scale(g.camScale, g.camScale)
// Center.
op.GeoM.Translate(float64(g.w/2.0), float64(g.h/2.0))
t.Draw(screen, op)
}
}
}

View File

@ -0,0 +1,93 @@
// Copyright 2021 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build example
// +build example
package main
import (
"fmt"
"math/rand"
"time"
)
// Level represents a Game level.
type Level struct {
w, h int
tiles [][]*Tile // (Y,X) array of tiles
tileSize int
}
// Tile returns the tile at the provided coordinates, or nil.
func (l *Level) Tile(x, y int) *Tile {
if x >= 0 && y >= 0 && x < l.w && y < l.h {
return l.tiles[y][x]
}
return nil
}
// Size returns the size of the Level.
func (l *Level) Size() (width, height int) {
return l.w, l.h
}
// NewLevel returns a new randomly generated Level.
func NewLevel() (*Level, error) {
// Create a 108x108 Level.
l := &Level{
w: 108,
h: 108,
tileSize: 64,
}
// Load embedded SpriteSheet.
ss, err := LoadSpriteSheet(l.tileSize)
if err != nil {
return nil, fmt.Errorf("failed to load embedded spritesheet: %s", err)
}
// Generate a unique permutation each time.
r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
// Fill each tile with one or more sprites randomly.
l.tiles = make([][]*Tile, l.h)
for y := 0; y < l.h; y++ {
l.tiles[y] = make([]*Tile, l.w)
for x := 0; x < l.w; x++ {
t := &Tile{}
isBorderSpace := x == 0 || y == 0 || x == l.w-1 || y == l.h-1
val := r.Intn(1000)
switch {
case isBorderSpace || val < 275:
t.AddSprite(ss.Wall)
case val < 285:
t.AddSprite(ss.Statue)
case val < 288:
t.AddSprite(ss.Crown)
case val < 289:
t.AddSprite(ss.Floor)
t.AddSprite(ss.Tube)
case val < 290:
t.AddSprite(ss.Portal)
default:
t.AddSprite(ss.Floor)
}
l.tiles[y][x] = t
}
}
return l, nil
}

View File

@ -0,0 +1,39 @@
// Copyright 2021 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build example
// +build example
package main
import (
"log"
"github.com/hajimehoshi/ebiten/v2"
)
func main() {
ebiten.SetWindowTitle("Isometric (Ebiten Demo)")
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowResizable(true)
g, err := NewGame()
if err != nil {
log.Fatal(err)
}
if err = ebiten.RunGame(g); err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,63 @@
// Copyright 2021 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build example
// +build example
package main
import (
"bytes"
"image"
_ "image/png"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
)
// SpriteSheet represents a collection of sprite images.
type SpriteSheet struct {
Floor *ebiten.Image
Wall *ebiten.Image
Statue *ebiten.Image
Tube *ebiten.Image
Crown *ebiten.Image
Portal *ebiten.Image
}
// LoadSpriteSheet loads the embedded SpriteSheet.
func LoadSpriteSheet(tileSize int) (*SpriteSheet, error) {
img, _, err := image.Decode(bytes.NewReader(images.Spritesheet_png))
if err != nil {
return nil, err
}
sheet := ebiten.NewImageFromImage(img)
// spriteAt returns a sprite at the provided coordinates.
spriteAt := func(x, y int) *ebiten.Image {
return sheet.SubImage(image.Rect(x*tileSize, (y+1)*tileSize, (x+1)*tileSize, y*tileSize)).(*ebiten.Image)
}
// Populate SpriteSheet.
s := &SpriteSheet{}
s.Floor = spriteAt(10, 4)
s.Wall = spriteAt(2, 3)
s.Statue = spriteAt(5, 4)
s.Tube = spriteAt(3, 4)
s.Crown = spriteAt(8, 6)
s.Portal = spriteAt(5, 6)
return s, nil
}

View File

@ -0,0 +1,48 @@
// Copyright 2021 The Ebiten Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build example
// +build example
package main
import (
"github.com/hajimehoshi/ebiten/v2"
)
// Tile represents a space with an x,y coordinate within a Level. Any number of
// sprites may be added to a Tile.
type Tile struct {
sprites []*ebiten.Image
}
// AddSprite adds a sprite to the Tile.
func (t *Tile) AddSprite(s *ebiten.Image) {
t.sprites = append(t.sprites, s)
}
// ClearSprites removes all sprites from the Tile.
func (t *Tile) ClearSprites() {
for i := range t.sprites {
t.sprites[i] = nil
}
t.sprites = t.sprites[:0]
}
// Draw draws the Tile on the screen using the provided options.
func (t *Tile) Draw(screen *ebiten.Image, options *ebiten.DrawImageOptions) {
for _, s := range t.sprites {
screen.DrawImage(s, options)
}
}

View File

@ -31,6 +31,13 @@ https://opengameart.org/content/runner-character
CC0 1.0
```
## spritesheet.png
```
Part of (or All) the graphic tiles used in this program is the public domain
roguelike tileset 'RLTiles'. You can find the original tileset at: http://rltiles.sf.net
```
## smoke.png
```

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB