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
This commit is contained in:
parent
2885b2c3d0
commit
ba97c81b55
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,
|
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"),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -107,15 +107,18 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
||||||
FillX: true,
|
FillX: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{
|
// Play Button, for levels only.
|
||||||
Text: "Play (P)",
|
if s.DrawingType == enum.LevelDrawing {
|
||||||
Font: balance.PlayButtonFont,
|
u.PlayButton = ui.NewButton("Play", ui.NewLabel(ui.Label{
|
||||||
}))
|
Text: "Play (P)",
|
||||||
u.PlayButton.Handle(ui.Click, func(ed ui.EventData) error {
|
Font: balance.PlayButtonFont,
|
||||||
u.Scene.Playtest()
|
}))
|
||||||
return nil
|
u.PlayButton.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
})
|
u.Scene.Playtest()
|
||||||
u.Supervisor.Add(u.PlayButton)
|
return nil
|
||||||
|
})
|
||||||
|
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())
|
||||||
u.PlayButton.Present(e, u.PlayButton.Point())
|
if u.PlayButton != nil {
|
||||||
|
u.PlayButton.Present(e, u.PlayButton.Point())
|
||||||
|
}
|
||||||
|
|
||||||
u.screen.Present(e, render.Origin)
|
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.
|
// 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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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 (
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user