Loading Screen

* pkg/loadscreen implements a global Loading Screen for loading heavy
  levels for playing or editing.
* All chunks in a level are pre-rendered to bitmap before gameplay
  begins, which reduces stutter as chunks were being lazily rendered on
  first appearance before.
* The loading screen can be played with in the developer console:
  $ loadscreen.Show()
  $ loadscreen.Hide()
  Along with ShowWithProgress(), SetProgress(float64) and IsActive()
* Chunker: separate the concerns between Bitmaps an (SDL2) Textures.
* Chunker.Prerender() converts a chunk to a bitmap (a Go image.Image)
  and caches it, only re-rendering if marked as dirty.
* Chunker.Texture() will use the pre-cached bitmap if available to
  immediately produce the SDL2 texture.

Other miscellaneous changes:

* Added to the Colored Pencil palette: Sandstone
* Added "perlin noise" brush pattern

Note: this commit introduces instability and crashes:

* New `asyncSetup()` functions run on a goroutine, but SDL2 texture
  calls must run on the main thread.
* Chunker avoids this by caching bitmaps, not textures.
* Wallpaper though is unstable, sometimes works, sometimes has graphical
  glitches, sometimes crashes the game.
* Wallpaper.Load() and the *Texture() functions are where it crashes.
This commit is contained in:
Noah 2021-07-18 20:04:24 -07:00
parent 2885b2c3d0
commit d4e6d9babb
13 changed files with 471 additions and 27 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -29,6 +29,22 @@ var (
Shadow: render.Black, Shadow: render.Black,
} }
// Loading Screen fonts.
LoadScreenFont = render.Text{
Size: 46,
Color: render.Pink,
Stroke: render.SkyBlue,
Shadow: render.Black,
}
LoadScreenSecondaryFont = render.Text{
FontFilename: "DejaVuSans.ttf",
Size: 18,
Color: render.SkyBlue,
Shadow: render.SkyBlue.Darken(128),
// Color: render.RGBA(255, 153, 0, 255),
// Shadow: render.RGBA(200, 80, 0, 255),
}
// Window and panel styles. // Window and panel styles.
TitleConfig = ui.Config{ TitleConfig = ui.Config{
Background: render.MustHexColor("#FF9900"), Background: render.MustHexColor("#FF9900"),

View File

@ -12,6 +12,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal" "git.kirsle.net/apps/doodle/pkg/modal"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/pattern" "git.kirsle.net/apps/doodle/pkg/pattern"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
@ -160,8 +161,9 @@ func (d *Doodle) Run() error {
DebugCollision = !DebugCollision DebugCollision = !DebugCollision
} }
// Is a UI modal active? // Make sure no UI modals (alerts, confirms)
if modal.Handled(ev) == false { // or loadscreen are currently visible.
if !modal.Handled(ev) {
// Run the scene's logic. // Run the scene's logic.
err = d.Scene.Loop(d, ev) err = d.Scene.Loop(d, ev)
if err != nil { if err != nil {
@ -174,6 +176,9 @@ func (d *Doodle) Run() error {
// Draw the scene. // Draw the scene.
d.Scene.Draw(d) d.Scene.Draw(d)
// Draw the loadscreen if it is active.
loadscreen.Loop(render.NewRect(d.width, d.height), d.Engine)
// Draw modals on top of the game UI. // Draw modals on top of the game UI.
modal.Draw() modal.Draw()

View File

@ -15,6 +15,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal" "git.kirsle.net/apps/doodle/pkg/modal"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -64,6 +65,21 @@ func (s *EditorScene) Setup(d *Doodle) error {
{"Swatch:", s.debSwatch}, {"Swatch:", s.debSwatch},
} }
// Show the loading screen.
loadscreen.ShowWithProgress()
go func() {
if err := s.setupAsync(d); err != nil {
log.Error("EditorScene.setupAsync: %s", err)
}
loadscreen.Hide()
}()
return nil
}
// setupAsync initializes trhe editor scene in the background,
// underneath a loading screen.
func (s *EditorScene) setupAsync(d *Doodle) error {
// Initialize the user interface. It references the palette and such so it // Initialize the user interface. It references the palette and such so it
// must be initialized after those things. // must be initialized after those things.
s.d = d s.d = d
@ -81,10 +97,17 @@ func (s *EditorScene) Setup(d *Doodle) error {
case enum.LevelDrawing: case enum.LevelDrawing:
if s.Level != nil { if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller") log.Debug("EditorScene.Setup: received level from scene caller")
loadscreen.SetSubtitle(
"Opening: "+s.Level.Title,
"by "+s.Level.Author,
)
s.UI.Canvas.LoadLevel(d.Engine, s.Level) s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.InstallActors(s.Level.Actors) s.UI.Canvas.InstallActors(s.Level.Actors)
} else if s.filename != "" && s.OpenFile { } else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
loadscreen.SetSubtitle(
"Opening: " + s.filename,
)
if err := s.LoadLevel(s.filename); err != nil { if err := s.LoadLevel(s.filename); err != nil {
d.Flash("LoadLevel error: %s", err) d.Flash("LoadLevel error: %s", err)
} else { } else {
@ -113,6 +136,12 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.UI.Canvas.ScrollTo(render.Origin) s.UI.Canvas.ScrollTo(render.Origin)
s.UI.Canvas.Scrollable = true s.UI.Canvas.Scrollable = true
} }
// Update the loading screen with level info.
loadscreen.SetSubtitle(
"Opening: "+s.Level.Title,
"by "+s.Level.Author,
)
case enum.DoodadDrawing: case enum.DoodadDrawing:
// Getting a doodad from file? // Getting a doodad from file?
if s.filename != "" && s.OpenFile { if s.filename != "" && s.OpenFile {
@ -140,6 +169,12 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.UI.Canvas.LoadDoodad(s.Doodad) s.UI.Canvas.LoadDoodad(s.Doodad)
} }
// Update the loading screen with level info.
loadscreen.SetSubtitle(
s.Doodad.Title,
"by "+s.Doodad.Author,
)
// TODO: move inside the UI. Just an approximate position for now. // TODO: move inside the UI. Just an approximate position for now.
s.UI.Canvas.Resize(render.NewRect(s.DoodadSize, s.DoodadSize)) s.UI.Canvas.Resize(render.NewRect(s.DoodadSize, s.DoodadSize))
s.UI.Canvas.ScrollTo(render.Origin) s.UI.Canvas.ScrollTo(render.Origin)
@ -147,10 +182,20 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.UI.Workspace.Compute(d.Engine) s.UI.Workspace.Compute(d.Engine)
} }
// Pre-cache all bitmap images from the level chunks.
// Note: we are not running on the main thread, so SDL2 Textures
// don't get created yet, but we do the full work of caching bitmap
// images which later get fed directly into SDL2 saving speed at
// runtime, + the bitmap generation is pretty wicked fast anyway.
loadscreen.PreloadAllChunkBitmaps(s.UI.Canvas.Chunker())
// Recompute the UI Palette window for the level's palette. // Recompute the UI Palette window for the level's palette.
s.UI.FinishSetup(d) s.UI.FinishSetup(d)
d.Flash("Editor Mode. Press 'P' to play this map.") d.Flash("Editor Mode.")
if s.DrawingType == enum.LevelDrawing {
d.Flash("Press 'P' to playtest this level.")
}
return nil return nil
} }
@ -180,6 +225,11 @@ func (s *EditorScene) ConfirmUnload(fn func()) {
// Loop the editor scene. // Loop the editor scene.
func (s *EditorScene) Loop(d *Doodle, ev *event.State) error { func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
// Skip if still loading.
if loadscreen.IsActive() {
return nil
}
// Update debug overlay values. // Update debug overlay values.
*s.debTool = s.UI.Canvas.Tool.String() *s.debTool = s.UI.Canvas.Tool.String()
*s.debSwatch = "???" *s.debSwatch = "???"
@ -253,7 +303,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
// s.UI.Loop(ev) // s.UI.Loop(ev)
// Switching to Play Mode? // Switching to Play Mode?
if keybind.GotoPlay(ev) { if s.DrawingType == enum.LevelDrawing && keybind.GotoPlay(ev) {
s.Playtest() s.Playtest()
} else if keybind.LineTool(ev) { } else if keybind.LineTool(ev) {
d.Flash("Line Tool selected.") d.Flash("Line Tool selected.")
@ -286,6 +336,11 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
// Draw the current frame. // Draw the current frame.
func (s *EditorScene) Draw(d *Doodle) error { func (s *EditorScene) Draw(d *Doodle) error {
// Skip if still loading.
if loadscreen.IsActive() {
return nil
}
// Clear the canvas and fill it with magenta so it's clear if any spots are missed. // Clear the canvas and fill it with magenta so it's clear if any spots are missed.
d.Engine.Clear(render.RGBA(160, 120, 160, 255)) d.Engine.Clear(render.RGBA(160, 120, 160, 255))

View File

@ -107,6 +107,8 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
FillX: true, FillX: true,
}) })
// Play Button, for levels only.
if s.DrawingType == enum.LevelDrawing {
u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{ u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{
Text: "Play (P)", Text: "Play (P)",
Font: balance.PlayButtonFont, Font: balance.PlayButtonFont,
@ -116,6 +118,7 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
return nil return nil
}) })
u.Supervisor.Add(u.PlayButton) u.Supervisor.Add(u.PlayButton)
}
// Position the Canvas inside the frame. // Position the Canvas inside the frame.
u.Workspace.Pack(u.Canvas, ui.Pack{ u.Workspace.Pack(u.Canvas, ui.Pack{
@ -234,7 +237,7 @@ func (u *EditorUI) Resized(d *Doodle) {
} }
// Position the Play button over the workspace. // Position the Play button over the workspace.
{ if u.PlayButton != nil {
btn := u.PlayButton btn := u.PlayButton
btn.Compute(d.Engine) btn.Compute(d.Engine)
@ -330,7 +333,9 @@ func (u *EditorUI) Present(e render.Engine) {
u.MenuBar.Present(e, u.MenuBar.Point()) u.MenuBar.Present(e, u.MenuBar.Point())
u.StatusBar.Present(e, u.StatusBar.Point()) u.StatusBar.Present(e, u.StatusBar.Point())
u.ToolBar.Present(e, u.ToolBar.Point()) u.ToolBar.Present(e, u.ToolBar.Point())
if u.PlayButton != nil {
u.PlayButton.Present(e, u.PlayButton.Point()) u.PlayButton.Present(e, u.PlayButton.Point())
}
u.screen.Present(e, render.Origin) u.screen.Present(e, render.Origin)

View File

@ -32,6 +32,7 @@ type Chunk struct {
// Texture cache properties so we don't redraw pixel-by-pixel every frame. // Texture cache properties so we don't redraw pixel-by-pixel every frame.
uuid uuid.UUID uuid uuid.UUID
bitmap image.Image
texture render.Texturer texture render.Texturer
textureMasked render.Texturer textureMasked render.Texturer
textureMaskedColor render.Color textureMaskedColor render.Color
@ -78,7 +79,7 @@ func NewChunk() *Chunk {
func (c *Chunk) Texture(e render.Engine) render.Texturer { func (c *Chunk) Texture(e render.Engine) render.Texturer {
if c.texture == nil || c.dirty { if c.texture == nil || c.dirty {
// Generate the normal bitmap and one with a color mask if applicable. // Generate the normal bitmap and one with a color mask if applicable.
tex, err := c.toBitmap(render.Invisible) tex, err := c.generateTexture(render.Invisible)
if err != nil { if err != nil {
log.Error("Texture: %s", err) log.Error("Texture: %s", err)
} }
@ -93,8 +94,9 @@ func (c *Chunk) Texture(e render.Engine) render.Texturer {
// TextureMasked returns a cached texture with the ColorMask applied. // TextureMasked returns a cached texture with the ColorMask applied.
func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texturer { func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texturer {
if c.textureMasked == nil || c.textureMaskedColor != mask { if c.textureMasked == nil || c.textureMaskedColor != mask {
// Generate the normal bitmap and one with a color mask if applicable. // Force regenerate with the new mask color.
tex, err := c.toBitmap(mask) c.dirty = true
tex, err := c.generateTexture(mask)
if err != nil { if err != nil {
log.Error("Texture: %s", err) log.Error("Texture: %s", err)
} }
@ -111,8 +113,24 @@ func (c *Chunk) SetDirty() {
c.dirty = true c.dirty = true
} }
// toBitmap puts the texture in a well named bitmap path in the cache folder. // CachedBitmap returns a cached render of the chunk as a bitmap image.
func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) { //
// This is like Texture() but skips the step of actually producing an
// (SDL2) texture. The benefit of this is that you can call it from
// your non-main threads and offload the bitmap work into background
// tasks, then when SDL2 needs the Texture, the cached bitmap is
// immediately there saving time on the main thread.
func (c *Chunk) CachedBitmap(mask render.Color) image.Image {
if c.bitmap == nil || c.dirty {
c.bitmap = c.ToBitmap(mask)
}
return c.bitmap
}
// generateTexture takes the chunk's Bitmap, turns it into an (SDL2)
// texture, and caches the texture in memory until the chunk is marked
// as dirty.
func (c *Chunk) generateTexture(mask render.Color) (render.Texturer, error) {
// Generate a unique name for this chunk cache. // Generate a unique name for this chunk cache.
var name string var name string
if c.uuid == uuid.Nil { if c.uuid == uuid.Nil {
@ -126,12 +144,21 @@ func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) {
) )
} }
// Get the temp bitmap image. // Get (and/or cache) the chunk to a bitmap image.
return c.ToBitmap(name, mask) // Note: the 1st call to Bitmap or after SetDirty will
// generate the image and store it cached.
bitmap := c.CachedBitmap(mask)
// Cache the texture data with the current renderer.
tex, err := shmem.CurrentRenderEngine.StoreTexture(name, bitmap)
return tex, err
} }
// ToBitmap exports the chunk's pixels as a bitmap image. // ToBitmap exports the chunk's pixels as a bitmap image.
func (c *Chunk) ToBitmap(filename string, mask render.Color) (render.Texturer, error) { // NOT CACHED! This will always run the logic. Use Bitmap() if you
// want a cached bitmap image that only generates itself once, and
// again when marked dirty.
func (c *Chunk) ToBitmap(mask render.Color) image.Image {
canvas := c.SizePositive() canvas := c.SizePositive()
imgSize := image.Rectangle{ imgSize := image.Rectangle{
Min: image.Point{}, Min: image.Point{},
@ -189,9 +216,7 @@ func (c *Chunk) ToBitmap(filename string, mask render.Color) (render.Texturer, e
) )
} }
// Cache the texture data with the current renderer. return img
tex, err := shmem.CurrentRenderEngine.StoreTexture(filename, img)
return tex, err
} }
// Set proxies to the accessor and flags the texture as dirty. // Set proxies to the accessor and flags the texture as dirty.

View File

@ -188,6 +188,43 @@ func (c *Chunker) Redraw() {
} }
} }
// Prerender visits every chunk and fetches its texture, in order to pre-load
// the whole drawing for smooth gameplay rather than chunks lazy rendering as
// they enter the screen.
func (c *Chunker) Prerender() {
for _, chunk := range c.Chunks {
_ = chunk.CachedBitmap(render.Invisible)
}
}
// PrerenderN will pre-render the texture for N number of chunks and then
// yield back to the caller. Returns the number of chunks that still need
// textures rendered; zero when the last chunk has been prerendered.
func (c *Chunker) PrerenderN(n int) (remaining int) {
var (
total int // total no. of chunks available
totalRendered int // no. of chunks with textures
modified int // number modified this call
)
for _, chunk := range c.Chunks {
total++
if chunk.bitmap != nil {
totalRendered++
continue
}
if modified < n {
_ = chunk.CachedBitmap(render.Invisible)
totalRendered++
modified++
}
}
remaining = total - totalRendered
return
}
// Get a pixel at the given coordinate. Returns the Palette entry for that // Get a pixel at the given coordinate. Returns the Palette entry for that
// pixel or else returns an error if not found. // pixel or else returns an error if not found.
func (c *Chunker) Get(p render.Point) (*Swatch, error) { func (c *Chunker) Get(p render.Point) (*Swatch, error) {

View File

@ -61,6 +61,12 @@ var (
Solid: true, Solid: true,
Pattern: "noise.png", Pattern: "noise.png",
}, },
{
Name: "sandstone",
Color: render.RGBA(215, 114, 44, 255),
Solid: true,
Pattern: "perlin-noise.png",
},
{ {
Name: "fire", Name: "fire",
Color: render.Red, Color: render.Red,

View File

@ -0,0 +1,231 @@
// Package loadscreen implements a modal "Loading" screen for the game, which
// can be shown or hidden by gameplay scenes as needed.
package loadscreen
import (
"strings"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
// Configuration values.
const (
ProgressWidth = 300
ProgressHeight = 34
)
// State variables for the loading screen.
var (
visible bool
withProgress bool
subtitle string // custom subtitle text, SetSubtitle().
// Animated title bar
titleBase = "Loading"
animState = 0
animation = []string{
". ",
".. ",
"...",
" ..",
" .",
" ",
}
animSpeed uint64 = 32
titleVar string
// UI widgets.
window *ui.Frame
canvas *uix.Canvas
secondary *ui.Label // subtitle text
progressTrough *ui.Frame
progressBar *ui.Frame
progressText *ui.Label
)
// Show the basic loading screen without a progress bar.
func Show() {
setup()
visible = true
withProgress = false
subtitle = ""
}
// ShowWithProgress initializes the loading screen with a progress bar starting at zero.
func ShowWithProgress() {
setup()
visible = true
withProgress = true
subtitle = ""
SetProgress(0)
}
// SetSubtitle specifies secondary text beneath the Loading banner.
// The subtitle is blanked on Show() and ShowWithProgress() and must
// be specified by the caller if desired. Pass multiple values for
// multiple lines of text.
func SetSubtitle(value ...string) {
subtitle = strings.Join(value, "\n")
}
// IsActive returns whether the loading screen is currently visible.
func IsActive() bool {
return visible
}
// Hide the loading screen.
func Hide() {
visible = false
}
// SetProgress sets the current progress value for loading screens having a progress bar.
func SetProgress(v float64) {
// Resize the progress bar in the trough.
if progressTrough != nil {
var (
troughSize = progressTrough.Size()
height = progressBar.Size().H
)
progressBar.Resize(render.Rect{
W: int(float64(troughSize.W-4) * v),
H: height,
})
}
}
// Common function to initialize the loading screen.
func setup() {
if window != nil {
return
}
titleVar = titleBase + animation[animState]
// Create the parent container that will stretch full screen.
window = ui.NewFrame("Loadscreen Window")
window.SetBackground(render.RGBA(0, 0, 1, 40))
// "Loading" text.
label := ui.NewLabel(ui.Label{
TextVariable: &titleVar,
Font: balance.LoadScreenFont,
})
label.Compute(shmem.CurrentRenderEngine)
window.Place(label, ui.Place{
Top: 128,
Center: true,
})
// Subtitle text.
secondary = ui.NewLabel(ui.Label{
TextVariable: &subtitle,
Font: balance.LoadScreenSecondaryFont,
})
window.Place(secondary, ui.Place{
Top: 128 + label.Size().H + 64,
Center: true,
})
// Progress bar.
progressTrough = ui.NewFrame("Progress Trough")
progressTrough.Configure(ui.Config{
Width: ProgressWidth,
Height: ProgressHeight,
BorderSize: 2,
BorderStyle: ui.BorderSunken,
Background: render.DarkGrey,
})
window.Place(progressTrough, ui.Place{
Center: true,
Middle: true,
})
progressBar = ui.NewFrame("Progress Bar")
progressBar.Configure(ui.Config{
Width: 0,
Height: ProgressHeight - 4,
Background: render.Green,
})
progressTrough.Pack(progressBar, ui.Pack{
Side: ui.W,
})
}
// Loop is called on every game loop. If the loadscreen is not active, nothing happens.
// Otherwise the loading screen UI is drawn to screen.
func Loop(windowSize render.Rect, e render.Engine) {
if !visible {
return
}
if window != nil {
// Initialize the wallpaper canvas?
if canvas == nil {
canvas = uix.NewCanvas(128, false)
canvas.LoadLevel(e, &level.Level{
Chunker: level.NewChunker(100),
Palette: level.NewPalette(),
PageType: level.Bounded,
Wallpaper: "blueprint.png",
})
}
canvas.Resize(windowSize)
canvas.Compute(e)
canvas.Present(e, render.Origin)
window.Resize(windowSize)
window.Compute(e)
window.Present(e, render.Origin)
// Show/hide the progress bar.
progressTrough.Compute(e)
if withProgress && progressTrough.Hidden() {
progressTrough.Show()
} else if !withProgress && !progressTrough.Hidden() {
progressTrough.Hide()
}
// Show/hide the subtitle text.
if len(subtitle) > 0 && secondary.Hidden() {
secondary.Show()
} else if subtitle == "" && !secondary.Hidden() {
secondary.Hide()
}
// Animate the ellipses.
if shmem.Tick%animSpeed == 0 {
titleVar = titleBase + animation[animState]
animState++
if animState >= len(animation) {
animState = 0
}
}
}
}
// PreloadAllChunkBitmaps is a helper function to eager cache all bitmap
// images of the chunks in a level drawing. It is designed to work with the
// loading screen and will set the Progress percent based on the total number
// of chunks vs. chunks remaining to pre-cache bitmaps from.
func PreloadAllChunkBitmaps(chunker *level.Chunker) {
loadChunksTarget := len(chunker.Chunks)
for {
remaining := chunker.PrerenderN(10)
// Set the load screen progress % based on number of chunks to render.
if loadChunksTarget > 0 {
percent := float64(loadChunksTarget-remaining) / float64(loadChunksTarget)
SetProgress(percent)
}
if remaining == 0 {
break
}
}
}

View File

@ -4,6 +4,7 @@ package modal
import ( import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/render/event" "git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
@ -44,7 +45,16 @@ func Reset() {
// Handled runs the modal manager's logic. Returns true if a modal // Handled runs the modal manager's logic. Returns true if a modal
// is presently active, to signal to Doodle not to run game logic. // is presently active, to signal to Doodle not to run game logic.
//
// This function also returns true if the pkg/modal/loadscreen is
// currently active.
func Handled(ev *event.State) bool { func Handled(ev *event.State) bool {
// The loadscreen counts as a modal for this purpose.
if loadscreen.IsActive() {
return true
}
// Check if we have a modal currently active.
if !ready || current == nil { if !ready || current == nil {
return false return false
} }

View File

@ -37,7 +37,11 @@ var Builtins = []Pattern{
Filename: "ink.png", Filename: "ink.png",
}, },
{ {
Name: "Dashed Lines", Name: "Perlin Noise",
Filename: "perlin-noise.png",
},
{
Name: "Bubbles",
Filename: "circles.png", Filename: "circles.png",
}, },
{ {

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/apps/doodle/pkg/physics"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/uix"
@ -75,6 +76,23 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.scripting = scripting.NewSupervisor() s.scripting = scripting.NewSupervisor()
s.supervisor = ui.NewSupervisor() s.supervisor = ui.NewSupervisor()
// Show the loading screen.
loadscreen.ShowWithProgress()
go func() {
if err := s.setupAsync(d); err != nil {
log.Error("PlayScene.setupAsync: %s", err)
return
}
loadscreen.Hide()
}()
return nil
}
// setupAsync initializes the play screen in the background, underneath
// a Loading screen.
func (s *PlayScene) setupAsync(d *Doodle) error {
// Create an invisible 'screen' frame for UI elements to use for positioning. // Create an invisible 'screen' frame for UI elements to use for positioning.
s.screen = ui.NewFrame("Screen") s.screen = ui.NewFrame("Screen")
s.screen.Resize(render.NewRect(d.width, d.height)) s.screen.Resize(render.NewRect(d.width, d.height))
@ -156,6 +174,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.drawing.LoadLevel(d.Engine, s.Level) s.drawing.LoadLevel(d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors) s.drawing.InstallActors(s.Level.Actors)
} else if s.Filename != "" { } else if s.Filename != "" {
loadscreen.SetSubtitle("Opening: " + s.Filename)
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
// NOTE: s.LoadLevel also calls s.drawing.InstallActors // NOTE: s.LoadLevel also calls s.drawing.InstallActors
s.LoadLevel(s.Filename) s.LoadLevel(s.Filename)
@ -168,6 +187,12 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.drawing.InstallActors(s.Level.Actors) s.drawing.InstallActors(s.Level.Actors)
} }
// Set the loading screen text with the level metadata.
loadscreen.SetSubtitle(
s.Level.Title,
"by "+s.Level.Author,
)
// Load all actor scripts. // Load all actor scripts.
s.drawing.SetScriptSupervisor(s.scripting) s.drawing.SetScriptSupervisor(s.scripting)
if err := s.scripting.InstallScripts(s.Level); err != nil { if err := s.scripting.InstallScripts(s.Level); err != nil {
@ -188,6 +213,13 @@ func (s *PlayScene) Setup(d *Doodle) error {
d.Flash("%s", s.Level.Title) d.Flash("%s", s.Level.Title)
} }
// Pre-cache all bitmap images from the level chunks.
// Note: we are not running on the main thread, so SDL2 Textures
// don't get created yet, but we do the full work of caching bitmap
// images which later get fed directly into SDL2 saving speed at
// runtime, + the bitmap generation is pretty wicked fast anyway.
loadscreen.PreloadAllChunkBitmaps(s.Level.Chunker)
s.running = true s.running = true
return nil return nil
@ -374,6 +406,11 @@ func (s *PlayScene) DieByFire(name string) {
// Loop the editor scene. // Loop the editor scene.
func (s *PlayScene) Loop(d *Doodle, ev *event.State) error { func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
// Skip if still loading.
if loadscreen.IsActive() {
return nil
}
// Update debug overlay values. // Update debug overlay values.
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).String() *s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).String()
*s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String() *s.debPosition = s.Player.Position().String() + " vel " + s.Player.Velocity().String()
@ -420,6 +457,11 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
// Draw the pixels on this frame. // Draw the pixels on this frame.
func (s *PlayScene) Draw(d *Doodle) error { func (s *PlayScene) Draw(d *Doodle) error {
// Skip if still loading.
if loadscreen.IsActive() {
return nil
}
// Clear the canvas and fill it with white. // Clear the canvas and fill it with white.
d.Engine.Clear(render.White) d.Engine.Clear(render.White)

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/render/event" "git.kirsle.net/go/render/event"
@ -85,6 +86,13 @@ func NewShell(d *Doodle) Shell {
} }
return "" return ""
}, },
"loadscreen": map[string]interface{}{
"Show": loadscreen.Show,
"ShowWithProgress": loadscreen.ShowWithProgress,
"Hide": loadscreen.Hide,
"IsActive": loadscreen.IsActive,
"SetProgress": loadscreen.SetProgress,
},
} }
for name, v := range bindings { for name, v := range bindings {
err := s.js.Set(name, v) err := s.js.Set(name, v)