Store image and audio assets in an atlas

This commit is contained in:
Trevor Slocum 2021-10-22 01:31:30 -07:00
parent 56fd9835ba
commit 066573dbf2
13 changed files with 257 additions and 197 deletions

View File

@ -0,0 +1,3 @@
These sprites were created by Penusbmic.
https://penusbmic.itch.io/dungeon-enemy-pack-3

View File

@ -0,0 +1,3 @@
These sprites were created by Chasersgaming.
https://chasersgaming.itch.io/rpg-characer-vampire-nes

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,3 @@
These tilesets were created by Ojas Sabadra.
https://ojas-sabadra.itch.io/dungeon-tileset-i

View File

@ -0,0 +1,3 @@
These tilesets were created by Kartoy.
https://kartoy.itch.io/32x32sandstone-dungeon-and-character-pack

View File

@ -1,6 +1,8 @@
package main
import (
"log"
"github.com/hajimehoshi/ebiten/v2/audio"
"github.com/hajimehoshi/ebiten/v2/audio/wav"
)
@ -16,7 +18,18 @@ const (
SoundGib
)
const numSounds = 7 // Must match above size.
var soundMap = map[int]string{
SoundGunshot: "assets/audio/gunshot.wav",
SoundVampireDie1: "assets/audio/vampiredie1.wav",
SoundVampireDie2: "assets/audio/vampiredie2.wav",
SoundBat: "assets/audio/bat.wav",
SoundPlayerHurt: "assets/audio/playerhurt.wav",
SoundPlayerDie: "assets/audio/playerdie.wav",
SoundMunch: "assets/audio/munch.wav",
}
var soundAtlas [][]*audio.Player
var nextSound = make([]int, len(soundMap))
func loadWav(context *audio.Context, p string) (*audio.Player, error) {
f, err := assetsFS.Open(p)
@ -32,3 +45,33 @@ func loadWav(context *audio.Context, p string) (*audio.Player, error) {
return context.NewPlayer(stream)
}
func loadStream(ctx *audio.Context, p string) (*audio.Player, error) {
stream, err := loadWav(ctx, p)
if err != nil {
return nil, err
}
// Workaround to prevent delays when playing for the first time.
stream.SetVolume(0)
stream.Play()
stream.Pause()
stream.Rewind()
return stream, nil
}
func loadSoundAtlas(ctx *audio.Context) [][]*audio.Player {
atlas := make([][]*audio.Player, len(soundMap))
var err error
for soundID, soundPath := range soundMap {
atlas[soundID] = make([]*audio.Player, 4)
for i := 0; i < 4; i++ {
atlas[soundID][i], err = loadStream(ctx, soundPath)
if err != nil {
log.Fatal(err)
}
}
}
return atlas
}

15
debug.go Normal file
View File

@ -0,0 +1,15 @@
//go:build debug
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
func init() {
go func() {
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}()
}

287
game.go
View File

@ -21,9 +21,6 @@ import (
"golang.org/x/text/message"
)
var bulletImage *ebiten.Image
var flashImage *ebiten.Image
var numberPrinter = message.NewPrinter(language.English)
var colorBlood = color.RGBA{102, 0, 0, 255}
@ -79,8 +76,13 @@ type game struct {
w, h int
level *Level
levelNum int
player *gamePlayer
requiredSouls int
spawnedPortal bool
gameStartTime time.Time
gameOverTime time.Time
@ -96,19 +98,10 @@ type game struct {
ojasSS *PlayerSpriteSheet
heartImg *ebiten.Image
vampireImage1 *ebiten.Image
vampireImage2 *ebiten.Image
vampireImage3 *ebiten.Image
garlicImage *ebiten.Image
holyWaterImage *ebiten.Image
overlayImg *ebiten.Image
op *ebiten.DrawImageOptions
audioContext *audio.Context
nextSound []int
soundBuffer [][]*audio.Player
lastBatSound time.Time
@ -123,10 +116,11 @@ type game struct {
flashMessageText string
flashMessageUntil time.Time
godMode bool
noclipMode bool
debugMode bool
cpuProfile *os.File
godMode bool
noclipMode bool
debugMode bool
fullBrightMode bool
cpuProfile *os.File
}
const sampleRate = 44100
@ -134,15 +128,13 @@ const sampleRate = 44100
// NewGame returns a new isometric demo game.
func NewGame() (*game, error) {
g := &game{
camScale: 2,
camScaleTo: 2,
mousePanX: math.MinInt32,
mousePanY: math.MinInt32,
op: &ebiten.DrawImageOptions{},
soundBuffer: make([][]*audio.Player, numSounds),
nextSound: make([]int, numSounds),
camScale: 2,
camScaleTo: 2,
mousePanX: math.MinInt32,
mousePanY: math.MinInt32,
activeGamepad: -1,
op: &ebiten.DrawImageOptions{},
}
g.audioContext = audio.NewContext(sampleRate)
@ -185,134 +177,15 @@ func (g *game) loadAssets() error {
return fmt.Errorf("failed to load embedded spritesheet: %s", err)
}
f, err := assetsFS.Open("assets/weapons/bullet.png")
if err != nil {
return err
}
img, _, err := image.Decode(f)
if err != nil {
return err
}
bulletImage = ebiten.NewImageFromImage(img)
f, err = assetsFS.Open("assets/weapons/flash.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
flashImage = ebiten.NewImageFromImage(img)
g.soundBuffer[SoundGunshot] = make([]*audio.Player, 4)
g.soundBuffer[SoundVampireDie1] = make([]*audio.Player, 4)
g.soundBuffer[SoundVampireDie2] = make([]*audio.Player, 4)
g.soundBuffer[SoundBat] = make([]*audio.Player, 4)
g.soundBuffer[SoundPlayerHurt] = make([]*audio.Player, 4)
g.soundBuffer[SoundPlayerDie] = make([]*audio.Player, 4)
g.soundBuffer[SoundMunch] = make([]*audio.Player, 4)
loadStream := func(p string) (*audio.Player, error) {
stream, err := loadWav(g.audioContext, p)
if err != nil {
return nil, err
}
// Workaround to prevent delays when playing for the first time.
stream.SetVolume(0)
stream.Play()
stream.Pause()
stream.Rewind()
return stream, nil
}
soundMap := map[int]string{
SoundGunshot: "assets/audio/gunshot.wav",
SoundVampireDie1: "assets/audio/vampiredie1.wav",
SoundVampireDie2: "assets/audio/vampiredie2.wav",
SoundBat: "assets/audio/bat.wav",
SoundPlayerHurt: "assets/audio/playerhurt.wav",
SoundPlayerDie: "assets/audio/playerdie.wav",
SoundMunch: "assets/audio/munch.wav",
}
for i := 0; i < 4; i++ {
for soundID, soundPath := range soundMap {
g.soundBuffer[soundID][i], err = loadStream(soundPath)
if err != nil {
return err
}
}
}
f, err = assetsFS.Open("assets/creeps/vampire1.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
g.vampireImage1 = ebiten.NewImageFromImage(img)
f, err = assetsFS.Open("assets/creeps/vampire2.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
g.vampireImage2 = ebiten.NewImageFromImage(img)
f, err = assetsFS.Open("assets/creeps/vampire3.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
g.vampireImage3 = ebiten.NewImageFromImage(img)
f, err = assetsFS.Open("assets/items/garlic.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
g.garlicImage = ebiten.NewImageFromImage(img)
f, err = assetsFS.Open("assets/items/holywater.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
g.holyWaterImage = ebiten.NewImageFromImage(img)
f, err = assetsFS.Open("assets/ui/heart.png")
if err != nil {
return err
}
img, _, err = image.Decode(f)
if err != nil {
return err
}
g.heartImg = ebiten.NewImageFromImage(img)
soundAtlas = loadSoundAtlas(g.audioContext)
return nil
}
func (g *game) newItem(itemType int) *gameItem {
sprite := g.garlicImage
sprite := imageAtlas[ImageGarlic]
if itemType == itemTypeHolyWater {
sprite = g.holyWaterImage
sprite = imageAtlas[ImageHolyWater]
}
x, y := g.level.newSpawnLocation()
return &gameItem{
@ -328,10 +201,10 @@ func (g *game) newItem(itemType int) *gameItem {
func (g *game) newCreep(creepType int) *gameCreep {
sprites := []*ebiten.Image{
g.vampireImage1,
g.vampireImage2,
g.vampireImage3,
g.vampireImage2,
imageAtlas[ImageVampire1],
imageAtlas[ImageVampire2],
imageAtlas[ImageVampire3],
imageAtlas[ImageVampire2],
}
if creepType == TypeBat {
sprites = []*ebiten.Image{
@ -364,37 +237,40 @@ func (g *game) newCreep(creepType int) *gameCreep {
}
}
func (g *game) reset() error {
log.Println("Starting a new game")
func (g *game) nextLevel() error {
g.levelNum++
if g.levelNum > 13 {
log.Fatal("YOU WIN")
}
return g.generateLevel()
}
g.tick = 0
func (g *game) generateLevel() error {
// Remove projectiles.
g.projectiles = nil
// Remove creeps.
if g.level != nil {
g.level.creeps = nil
}
var err error
g.level, err = NewLevel()
g.level, err = NewLevel(g.levelNum)
if err != nil {
return fmt.Errorf("failed to create new level: %s", err)
}
g.level.player = g.player
// Reset player score.
g.player.score = 0
// Reset player health.
g.player.health = 3
// Position player.
for {
g.player.x = float64(rand.Intn(108))
g.player.y = float64(rand.Intn(108))
g.player.x = float64(rand.Intn(g.level.w))
g.player.y = float64(rand.Intn(g.level.h))
if g.level.isFloor(g.player.x, g.player.y, false) {
break
}
}
// Remove projectiles.
g.projectiles = nil
// Spawn items.
g.level.items = nil
added := make(map[string]bool)
@ -447,6 +323,30 @@ func (g *game) reset() error {
return nil
}
func (g *game) reset() error {
log.Println("Starting a new game")
g.tick = 0
g.levelNum = 1
err := g.generateLevel()
if err != nil {
return err
}
// Reset player score.
g.player.score = 0
// Reset souls rescued.
g.player.soulsRescued = 0
// Reset player health.
g.player.health = 3
return nil
}
// Layout is called when the game's layout changes.
func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
s := ebiten.DeviceScaleFactor()
@ -593,6 +493,24 @@ func (g *game) Update() error {
g.camScaleTo = 4
} TODO */
// Update target zoom level.
if g.debugMode {
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)
}
// Smooth zoom transition.
div := 10.0
if g.camScaleTo > g.camScale {
@ -834,6 +752,21 @@ UPDATEPROJECTILES:
g.player.holyWaters++
g.flashMessage("+ HOLY WATER")
}
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.Key6) {
g.fullBrightMode = !g.fullBrightMode
if g.fullBrightMode {
g.flashMessage("FULLBRIGHT MODE ACTIVATED")
} else {
g.flashMessage("FULLBRIGHT MODE DEACTIVATED")
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyEqual) {
err := g.nextLevel()
if err != nil {
return err
}
g.flashMessage(fmt.Sprintf("WARPED TO LEVEL %d", g.levelNum))
}
if inpututil.IsKeyJustPressed(ebiten.KeyV) {
g.debugMode = !g.debugMode
if g.debugMode {
@ -922,7 +855,7 @@ func (g *game) Draw(screen *ebiten.Image) {
for i := 0; i < g.player.health; i++ {
g.op.GeoM.Reset()
g.op.GeoM.Translate(float64(heartX+(i*heartSpace)), 32)
screen.DrawImage(g.heartImg, g.op)
screen.DrawImage(imageAtlas[ImageHeart], g.op)
}
holyWaterSpace := 16
@ -930,7 +863,7 @@ func (g *game) Draw(screen *ebiten.Image) {
for i := 0; i < g.player.holyWaters; i++ {
g.op.GeoM.Reset()
g.op.GeoM.Translate(float64(holyWaterX+(i*holyWaterSpace)), 76)
screen.DrawImage(g.holyWaterImage, g.op)
screen.DrawImage(imageAtlas[ImageHolyWater], g.op)
}
scoreLabel := numberPrinter.Sprintf("%d", g.player.score)
@ -1002,6 +935,10 @@ func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float
// Calculate color scale to apply shadows.
func (g *game) colorScale(x, y float64) float64 {
if g.fullBrightMode {
return 1
}
dx, dy := deltaXY(x, y, g.player.x, g.player.y)
sD := 7 / (dx + dy)
@ -1067,7 +1004,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int {
}
for _, p := range g.projectiles {
drawn += g.renderSprite(p.x, p.y, 0, 0, p.angle, 1.0, g.colorScale(p.x, p.y), 1.0, bulletImage, screen)
drawn += g.renderSprite(p.x, p.y, 0, 0, p.angle, 1.0, g.colorScale(p.x, p.y), 1.0, imageAtlas[ImageBullet], screen)
}
repelTime := g.player.garlicUntil.Sub(time.Now())
@ -1078,7 +1015,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int {
if repelTime.Seconds() < 3 {
alpha = repelTime.Seconds() / 12
}
drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, 1.0, alpha, g.garlicImage, screen)
drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, 1.0, alpha, imageAtlas[ImageGarlic], screen)
}
holyWaterTime := g.player.holyWaterUntil.Sub(time.Now())
@ -1089,7 +1026,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int {
if holyWaterTime.Seconds() < 3 {
alpha = holyWaterTime.Seconds() / 2
}
drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, 1.0, alpha, g.holyWaterImage, screen)
drawn += g.renderSprite(g.player.x+0.25, g.player.y+0.25, -offset, -offset, 0, scale, 1.0, alpha, imageAtlas[ImageHolyWater], screen)
}
playerSprite := g.ojasSS.Frame1
@ -1109,7 +1046,7 @@ func (g *game) renderLevel(screen *ebiten.Image) int {
flashDuration := 40 * time.Millisecond
if time.Since(g.player.weapon.lastFire) < flashDuration {
drawn += g.renderSprite(g.player.x, g.player.y, 39, -1, g.player.angle, 1.0, 1.0, 1.0, flashImage, screen)
drawn += g.renderSprite(g.player.x, g.player.y, 39, -1, g.player.angle, 1.0, 1.0, 1.0, imageAtlas[ImageMuzzleFlash], screen)
}
return drawn
@ -1125,10 +1062,10 @@ func (g *game) resetExpiredTimers() {
}
func (g *game) playSound(sound int, volume float64) error {
player := g.soundBuffer[sound][g.nextSound[sound]]
g.nextSound[sound]++
if g.nextSound[sound] > 3 {
g.nextSound[sound] = 0
player := soundAtlas[sound][nextSound[sound]]
nextSound[sound]++
if nextSound[sound] > 3 {
nextSound[sound] = 0
}
player.Pause()
player.Rewind()

58
img.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"image"
"log"
"github.com/hajimehoshi/ebiten/v2"
)
const (
ImageVampire1 = iota
ImageVampire2
ImageVampire3
ImageGarlic
ImageHolyWater
ImageHeart
ImageUzi
ImageBullet
ImageMuzzleFlash
)
var imageMap = map[int]string{
ImageVampire1: "assets/creeps/vampire/vampire1.png",
ImageVampire2: "assets/creeps/vampire/vampire2.png",
ImageVampire3: "assets/creeps/vampire/vampire3.png",
ImageGarlic: "assets/items/garlic.png",
ImageHolyWater: "assets/items/holywater.png",
ImageHeart: "assets/ui/heart.png",
ImageUzi: "assets/weapons/uzi.png",
ImageBullet: "assets/weapons/bullet.png",
ImageMuzzleFlash: "assets/weapons/flash.png",
}
var imageAtlas = loadAtlas()
func loadImage(p string) (*ebiten.Image, error) {
f, err := assetsFS.Open(p)
if err != nil {
return nil, err
}
img, _, err := image.Decode(f)
if err != nil {
return nil, err
}
return ebiten.NewImageFromImage(img), nil
}
func loadAtlas() []*ebiten.Image {
atlas := make([]*ebiten.Image, len(imageMap))
var err error
for imgID, imgPath := range imageMap {
atlas[imgID], err = loadImage(imgPath)
if err != nil {
log.Fatal(err)
}
}
return atlas
}

View File

@ -97,11 +97,14 @@ SPAWNLOCATION:
}
// NewLevel returns a new randomly generated Level.
func NewLevel() (*Level, error) {
// Create a 216x216 Level.
func NewLevel(levelNum int) (*Level, error) {
multiplier := levelNum
if multiplier > 2 {
multiplier = 2
}
l := &Level{
w: 216,
h: 216,
w: 336 * multiplier,
h: 336 * multiplier,
tileSize: 32,
}
@ -110,14 +113,18 @@ func NewLevel() (*Level, error) {
return nil, fmt.Errorf("failed to load embedded spritesheet: %s", err)
}
dungeon := dungeon.NewDungeon(l.w/dungeonScale, 13)
rooms := 33
if multiplier == 2 {
rooms = 66
}
d := dungeon.NewDungeon(l.w/dungeonScale, rooms)
dungeonFloor := 1
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{}
if y < l.h-1 && dungeon.Grid[x/dungeonScale][y/dungeonScale] == dungeonFloor {
if y < l.h-1 && d.Grid[x/dungeonScale][y/dungeonScale] == dungeonFloor {
if rand.Intn(13) == 0 {
t.AddSprite(sandstoneSS.FloorC)
} else {

View File

@ -1,10 +1,7 @@
package main
import (
"image"
"time"
"github.com/hajimehoshi/ebiten/v2"
)
type gamePlayer struct {
@ -16,6 +13,8 @@ type gamePlayer struct {
score int
soulsRescued int
health int
holyWaters int
@ -25,20 +24,9 @@ type gamePlayer struct {
}
func NewPlayer() (*gamePlayer, error) {
f, err := assetsFS.Open("assets/weapons/uzi.png")
if err != nil {
return nil, err
}
img, _, err := image.Decode(f)
if err != nil {
return nil, err
}
uziSprite := ebiten.NewImageFromImage(img)
p := &gamePlayer{
weapon: &playerWeapon{
sprite: uziSprite,
sprite: imageAtlas[ImageUzi],
cooldown: 100 * time.Millisecond,
},
health: 3,