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:
parent
2885b2c3d0
commit
d4e6d9babb
BIN
assets/pattern/perlin-noise.png
Normal file
BIN
assets/pattern/perlin-noise.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -29,6 +29,22 @@ var (
|
|||
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.
|
||||
TitleConfig = ui.Config{
|
||||
Background: render.MustHexColor("#FF9900"),
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"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/pattern"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
|
@ -160,8 +161,9 @@ func (d *Doodle) Run() error {
|
|||
DebugCollision = !DebugCollision
|
||||
}
|
||||
|
||||
// Is a UI modal active?
|
||||
if modal.Handled(ev) == false {
|
||||
// Make sure no UI modals (alerts, confirms)
|
||||
// or loadscreen are currently visible.
|
||||
if !modal.Handled(ev) {
|
||||
// Run the scene's logic.
|
||||
err = d.Scene.Loop(d, ev)
|
||||
if err != nil {
|
||||
|
@ -174,6 +176,9 @@ func (d *Doodle) Run() error {
|
|||
// Draw the scene.
|
||||
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.
|
||||
modal.Draw()
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"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/userdir"
|
||||
"git.kirsle.net/go/render"
|
||||
|
@ -64,6 +65,21 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
{"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
|
||||
// must be initialized after those things.
|
||||
s.d = d
|
||||
|
@ -81,10 +97,17 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
case enum.LevelDrawing:
|
||||
if s.Level != nil {
|
||||
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.InstallActors(s.Level.Actors)
|
||||
} else if s.filename != "" && s.OpenFile {
|
||||
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 {
|
||||
d.Flash("LoadLevel error: %s", err)
|
||||
} else {
|
||||
|
@ -113,6 +136,12 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
s.UI.Canvas.ScrollTo(render.Origin)
|
||||
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:
|
||||
// Getting a doodad from file?
|
||||
if s.filename != "" && s.OpenFile {
|
||||
|
@ -140,6 +169,12 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
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.
|
||||
s.UI.Canvas.Resize(render.NewRect(s.DoodadSize, s.DoodadSize))
|
||||
s.UI.Canvas.ScrollTo(render.Origin)
|
||||
|
@ -147,10 +182,20 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
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.
|
||||
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
|
||||
}
|
||||
|
@ -180,6 +225,11 @@ func (s *EditorScene) ConfirmUnload(fn func()) {
|
|||
|
||||
// Loop the editor scene.
|
||||
func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
|
||||
// Skip if still loading.
|
||||
if loadscreen.IsActive() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update debug overlay values.
|
||||
*s.debTool = s.UI.Canvas.Tool.String()
|
||||
*s.debSwatch = "???"
|
||||
|
@ -253,7 +303,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
|
|||
// s.UI.Loop(ev)
|
||||
|
||||
// Switching to Play Mode?
|
||||
if keybind.GotoPlay(ev) {
|
||||
if s.DrawingType == enum.LevelDrawing && keybind.GotoPlay(ev) {
|
||||
s.Playtest()
|
||||
} else if keybind.LineTool(ev) {
|
||||
d.Flash("Line Tool selected.")
|
||||
|
@ -286,6 +336,11 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
|
|||
|
||||
// Draw the current frame.
|
||||
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.
|
||||
d.Engine.Clear(render.RGBA(160, 120, 160, 255))
|
||||
|
||||
|
|
|
@ -107,15 +107,18 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
|||
FillX: true,
|
||||
})
|
||||
|
||||
u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{
|
||||
Text: "Play (P)",
|
||||
Font: balance.PlayButtonFont,
|
||||
}))
|
||||
u.PlayButton.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
u.Scene.Playtest()
|
||||
return nil
|
||||
})
|
||||
u.Supervisor.Add(u.PlayButton)
|
||||
// Play Button, for levels only.
|
||||
if s.DrawingType == enum.LevelDrawing {
|
||||
u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{
|
||||
Text: "Play (P)",
|
||||
Font: balance.PlayButtonFont,
|
||||
}))
|
||||
u.PlayButton.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
u.Scene.Playtest()
|
||||
return nil
|
||||
})
|
||||
u.Supervisor.Add(u.PlayButton)
|
||||
}
|
||||
|
||||
// Position the Canvas inside the frame.
|
||||
u.Workspace.Pack(u.Canvas, ui.Pack{
|
||||
|
@ -234,7 +237,7 @@ func (u *EditorUI) Resized(d *Doodle) {
|
|||
}
|
||||
|
||||
// Position the Play button over the workspace.
|
||||
{
|
||||
if u.PlayButton != nil {
|
||||
btn := u.PlayButton
|
||||
btn.Compute(d.Engine)
|
||||
|
||||
|
@ -330,7 +333,9 @@ func (u *EditorUI) Present(e render.Engine) {
|
|||
u.MenuBar.Present(e, u.MenuBar.Point())
|
||||
u.StatusBar.Present(e, u.StatusBar.Point())
|
||||
u.ToolBar.Present(e, u.ToolBar.Point())
|
||||
u.PlayButton.Present(e, u.PlayButton.Point())
|
||||
if u.PlayButton != nil {
|
||||
u.PlayButton.Present(e, u.PlayButton.Point())
|
||||
}
|
||||
|
||||
u.screen.Present(e, render.Origin)
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ type Chunk struct {
|
|||
|
||||
// Texture cache properties so we don't redraw pixel-by-pixel every frame.
|
||||
uuid uuid.UUID
|
||||
bitmap image.Image
|
||||
texture render.Texturer
|
||||
textureMasked render.Texturer
|
||||
textureMaskedColor render.Color
|
||||
|
@ -78,7 +79,7 @@ func NewChunk() *Chunk {
|
|||
func (c *Chunk) Texture(e render.Engine) render.Texturer {
|
||||
if c.texture == nil || c.dirty {
|
||||
// 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 {
|
||||
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.
|
||||
func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texturer {
|
||||
if c.textureMasked == nil || c.textureMaskedColor != mask {
|
||||
// Generate the normal bitmap and one with a color mask if applicable.
|
||||
tex, err := c.toBitmap(mask)
|
||||
// Force regenerate with the new mask color.
|
||||
c.dirty = true
|
||||
tex, err := c.generateTexture(mask)
|
||||
if err != nil {
|
||||
log.Error("Texture: %s", err)
|
||||
}
|
||||
|
@ -111,8 +113,24 @@ func (c *Chunk) SetDirty() {
|
|||
c.dirty = true
|
||||
}
|
||||
|
||||
// toBitmap puts the texture in a well named bitmap path in the cache folder.
|
||||
func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) {
|
||||
// CachedBitmap returns a cached render of the chunk as a bitmap image.
|
||||
//
|
||||
// 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.
|
||||
var name string
|
||||
if c.uuid == uuid.Nil {
|
||||
|
@ -126,12 +144,21 @@ func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) {
|
|||
)
|
||||
}
|
||||
|
||||
// Get the temp bitmap image.
|
||||
return c.ToBitmap(name, mask)
|
||||
// Get (and/or cache) the chunk to a bitmap image.
|
||||
// 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.
|
||||
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()
|
||||
imgSize := image.Rectangle{
|
||||
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.
|
||||
tex, err := shmem.CurrentRenderEngine.StoreTexture(filename, img)
|
||||
return tex, err
|
||||
return img
|
||||
}
|
||||
|
||||
// Set proxies to the accessor and flags the texture as dirty.
|
||||
|
|
|
@ -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
|
||||
// pixel or else returns an error if not found.
|
||||
func (c *Chunker) Get(p render.Point) (*Swatch, error) {
|
||||
|
|
|
@ -61,6 +61,12 @@ var (
|
|||
Solid: true,
|
||||
Pattern: "noise.png",
|
||||
},
|
||||
{
|
||||
Name: "sandstone",
|
||||
Color: render.RGBA(215, 114, 44, 255),
|
||||
Solid: true,
|
||||
Pattern: "perlin-noise.png",
|
||||
},
|
||||
{
|
||||
Name: "fire",
|
||||
Color: render.Red,
|
||||
|
|
231
pkg/modal/loadscreen/loadscreen.go
Normal file
231
pkg/modal/loadscreen/loadscreen.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ package modal
|
|||
import (
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"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/event"
|
||||
"git.kirsle.net/go/ui"
|
||||
|
@ -44,7 +45,16 @@ func Reset() {
|
|||
|
||||
// Handled runs the modal manager's logic. Returns true if a modal
|
||||
// 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 {
|
||||
// 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 {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -37,7 +37,11 @@ var Builtins = []Pattern{
|
|||
Filename: "ink.png",
|
||||
},
|
||||
{
|
||||
Name: "Dashed Lines",
|
||||
Name: "Perlin Noise",
|
||||
Filename: "perlin-noise.png",
|
||||
},
|
||||
{
|
||||
Name: "Bubbles",
|
||||
Filename: "circles.png",
|
||||
},
|
||||
{
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"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/scripting"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
|
@ -75,6 +76,23 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
s.scripting = scripting.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.
|
||||
s.screen = ui.NewFrame("Screen")
|
||||
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.InstallActors(s.Level.Actors)
|
||||
} else if s.Filename != "" {
|
||||
loadscreen.SetSubtitle("Opening: " + s.Filename)
|
||||
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
|
||||
// NOTE: s.LoadLevel also calls s.drawing.InstallActors
|
||||
s.LoadLevel(s.Filename)
|
||||
|
@ -168,6 +187,12 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
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.
|
||||
s.drawing.SetScriptSupervisor(s.scripting)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
return nil
|
||||
|
@ -374,6 +406,11 @@ func (s *PlayScene) DieByFire(name string) {
|
|||
|
||||
// Loop the editor scene.
|
||||
func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
|
||||
// Skip if still loading.
|
||||
if loadscreen.IsActive() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update debug overlay values.
|
||||
*s.debWorldIndex = s.drawing.WorldIndexAt(render.NewPoint(ev.CursorX, ev.CursorY)).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.
|
||||
func (s *PlayScene) Draw(d *Doodle) error {
|
||||
// Skip if still loading.
|
||||
if loadscreen.IsActive() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear the canvas and fill it with white.
|
||||
d.Engine.Clear(render.White)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||
"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/go/render"
|
||||
"git.kirsle.net/go/render/event"
|
||||
|
@ -85,6 +86,13 @@ func NewShell(d *Doodle) Shell {
|
|||
}
|
||||
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 {
|
||||
err := s.js.Set(name, v)
|
||||
|
|
Loading…
Reference in New Issue
Block a user