1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2025-02-20 15:37:31 -05:00

229 lines
6.0 KiB
Go
Raw Normal View History

2020-10-28 15:49:49 -07:00
package d2systems
import (
"errors"
"image/color"
"os"
"sort"
"time"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
2020-10-28 15:49:49 -07:00
"github.com/gravestench/akara"
"github.com/hajimehoshi/ebiten/v2"
2020-10-28 15:49:49 -07:00
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2components"
d2render "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten"
)
const (
gameTitle = "Open Diablo 2"
logPrefixRenderSystem = "Render System"
2020-10-28 15:49:49 -07:00
)
// static check that RenderSystem implements the System interface
var _ akara.System = &RenderSystem{}
// RenderSystem is responsible for rendering the main viewports of scenes
// to the game screen.
2020-10-28 15:49:49 -07:00
type RenderSystem struct {
akara.BaseSubscriberSystem
*d2util.Logger
renderer d2interface.Renderer
viewports *akara.Subscription
configs *akara.Subscription
lastUpdate time.Time
Components struct {
GameConfig d2components.GameConfigFactory
Viewport d2components.ViewportFactory
MainViewport d2components.MainViewportFactory
Texture d2components.TextureFactory
Priority d2components.PriorityFactory
Alpha d2components.AlphaFactory
Camera d2components.CameraFactory
}
2020-10-28 15:49:49 -07:00
}
// Init initializes the system with the given world, injecting the necessary components
func (m *RenderSystem) Init(world *akara.World) {
m.World = world
m.lastUpdate = time.Now()
m.setupLogger()
changes to d2components, d2systems, d2ui, d2enum go.mod, go.sum: * updating akara, bugfix in akara.EntityManager.RemoveEntity d2core * adding d2core/d2label * adding d2core/d2bitmapfont d2ui * exporting some constants for use elsewhere d2components * added bitmap font component (for ui labels) * added FileLoaded tag component to simplify asset loading filters * added locale component * FilePath component renamed to File * sprite component now contains the sprite and palette path as strings * adding ui label component d2enum * added locale as file type for file "/data/local/use" d2systems * changed most info prints to debug prints * removed unused scene graph testing file (oops!) * terminal is now rendered above mouse cursor scene * adding ui widget system for use by the game object factory * adding test scene for ui labels created with the ui widget system d2systems/AppBootstrap * added command line args for profiler * `--testscene labels` launches the label test * now adds the local file for processing * game loop init logic now inside of Init method (the call to world.Update does this) d2systems/AssetLoader * loads the locale file and adds a locale component that other systems can use * adds a FileLoaded component after finished loading a file which other systems can use (like the loading scene) d2systems/FileSourceResolver * Now looks for and uses the locale for language/charset filepath substitution d2systems/GameClientBootstrap * game loop init moved to end of AppBootstrap.Init d2systems/GameObjectFactory * embedding UI widget factory system d2systems/BaseScene * made base scene a little more clear by breaking the process into more methods d2systems/LoadingScene * simplified the entity subscriptions by using the new FileLoaded component d2systems/SceneObjectFactory * adding method for adding labels, buttons to scenes (buttons still WIP) d2systems/SceneSpriteSystem * the sprite system now maintains a cache of rendered sprites
2020-12-08 10:42:19 -08:00
m.Debug("initializing ...")
m.setupFactories()
m.setupSubscriptions()
}
func (m *RenderSystem) setupLogger() {
m.Logger = d2util.NewLogger()
m.SetPrefix(logPrefixRenderSystem)
}
func (m *RenderSystem) setupFactories() {
m.InjectComponent(&d2components.GameConfig{}, &m.Components.GameConfig.ComponentFactory)
m.InjectComponent(&d2components.Viewport{}, &m.Components.Viewport.ComponentFactory)
m.InjectComponent(&d2components.MainViewport{}, &m.Components.MainViewport.ComponentFactory)
m.InjectComponent(&d2components.Texture{}, &m.Components.Texture.ComponentFactory)
m.InjectComponent(&d2components.Priority{}, &m.Components.Priority.ComponentFactory)
m.InjectComponent(&d2components.Alpha{}, &m.Components.Alpha.ComponentFactory)
m.InjectComponent(&d2components.Camera{}, &m.Components.Camera.ComponentFactory)
}
func (m *RenderSystem) setupSubscriptions() {
viewports := m.NewComponentFilter().
Require(
&d2components.Viewport{},
&d2components.MainViewport{},
&d2components.Texture{},
&d2components.Camera{},
).
Build()
gameConfigs := m.NewComponentFilter().
Require(&d2components.GameConfig{}).
Build()
2020-10-28 15:49:49 -07:00
m.viewports = m.AddSubscription(viewports)
m.configs = m.AddSubscription(gameConfigs)
2020-10-28 15:49:49 -07:00
}
// Update will initialize the renderer, start the game loop, and
// disable the system (to prevent it from being called during the game loop).
//
// The reason why this isn't in the init step is because we use other sceneSystems
// for loading the config file, and it may take more than one iteration
func (m *RenderSystem) Update() {
if m.renderer != nil {
return // we already created the renderer
2020-10-28 15:49:49 -07:00
}
m.createRenderer()
if m.renderer == nil {
return // the renderer has not yet been created!
}
// if we have created the renderer, we can safely disable
// this system and start the run loop.
m.SetActive(false)
err := m.StartGameLoop()
if err != nil {
m.Fatal(err.Error())
}
os.Exit(0)
2020-10-28 15:49:49 -07:00
}
func (m *RenderSystem) createRenderer() {
changes to d2components, d2systems, d2ui, d2enum go.mod, go.sum: * updating akara, bugfix in akara.EntityManager.RemoveEntity d2core * adding d2core/d2label * adding d2core/d2bitmapfont d2ui * exporting some constants for use elsewhere d2components * added bitmap font component (for ui labels) * added FileLoaded tag component to simplify asset loading filters * added locale component * FilePath component renamed to File * sprite component now contains the sprite and palette path as strings * adding ui label component d2enum * added locale as file type for file "/data/local/use" d2systems * changed most info prints to debug prints * removed unused scene graph testing file (oops!) * terminal is now rendered above mouse cursor scene * adding ui widget system for use by the game object factory * adding test scene for ui labels created with the ui widget system d2systems/AppBootstrap * added command line args for profiler * `--testscene labels` launches the label test * now adds the local file for processing * game loop init logic now inside of Init method (the call to world.Update does this) d2systems/AssetLoader * loads the locale file and adds a locale component that other systems can use * adds a FileLoaded component after finished loading a file which other systems can use (like the loading scene) d2systems/FileSourceResolver * Now looks for and uses the locale for language/charset filepath substitution d2systems/GameClientBootstrap * game loop init moved to end of AppBootstrap.Init d2systems/GameObjectFactory * embedding UI widget factory system d2systems/BaseScene * made base scene a little more clear by breaking the process into more methods d2systems/LoadingScene * simplified the entity subscriptions by using the new FileLoaded component d2systems/SceneObjectFactory * adding method for adding labels, buttons to scenes (buttons still WIP) d2systems/SceneSpriteSystem * the sprite system now maintains a cache of rendered sprites
2020-12-08 10:42:19 -08:00
m.Debug("creating renderer instance")
2020-10-28 15:49:49 -07:00
configs := m.configs.GetEntities()
if len(configs) < 1 {
return
}
config, found := m.Components.GameConfig.Get(configs[0])
2020-10-28 15:49:49 -07:00
if !found {
return
}
// we should get rid of d2config.Configuration and use components instead...
oldStyleConfig := &d2config.Configuration{
MpqLoadOrder: config.MpqLoadOrder,
MpqPath: config.MpqPath,
TicksPerSecond: config.TicksPerSecond,
FpsCap: config.FpsCap,
SfxVolume: config.SfxVolume,
BgmVolume: config.BgmVolume,
FullScreen: config.FullScreen,
RunInBackground: config.RunInBackground,
VsyncEnabled: config.VsyncEnabled,
Backend: config.Backend,
LogLevel: config.LogLevel,
}
renderer, err := d2render.CreateRenderer(oldStyleConfig)
2020-10-28 15:49:49 -07:00
if err != nil {
m.Fatal(err.Error())
2020-10-28 15:49:49 -07:00
}
// HACK: hardcoded with ebiten for now
ebiten.SetCursorMode(ebiten.CursorModeHidden)
ebiten.SetFullscreen(config.FullScreen)
ebiten.SetRunnableOnUnfocused(config.RunInBackground)
ebiten.SetVsyncEnabled(config.VsyncEnabled)
ebiten.SetMaxTPS(config.TicksPerSecond)
m.renderer = renderer
}
func (m *RenderSystem) render(screen d2interface.Surface) error {
entities := m.viewports.GetEntities()
sort.Slice(entities, func(i, j int) bool {
pi, pj := m.Components.Priority.Add(entities[i]), m.Components.Priority.Add(entities[j])
return pi.Priority < pj.Priority
})
for _, id := range entities {
vp, found := m.Components.Viewport.Get(id)
if !found {
return errors.New("main viewport not found")
}
2020-10-28 15:49:49 -07:00
cam, found := m.Components.Camera.Get(id)
if !found {
return errors.New("main viewport camera not found")
}
texture, found := m.Components.Texture.Get(id)
if !found {
return errors.New("main viewport doesn't have a surface")
}
2020-12-04 00:11:19 -08:00
if texture.Texture == nil {
texture.Texture = m.renderer.NewSurface(vp.Width, vp.Height)
}
alpha, found := m.Components.Alpha.Get(id)
if !found {
alpha = m.Components.Alpha.Add(id)
}
const maxAlpha = 255
screen.PushColor(color.Alpha{A: uint8(alpha.Alpha * maxAlpha)})
screen.PushTranslation(vp.Left, vp.Top)
screen.PushScale(float64(vp.Width)/cam.Size.X, float64(vp.Height)/cam.Size.Y)
2020-12-04 00:11:19 -08:00
screen.Render(texture.Texture)
screen.Pop()
screen.Pop()
screen.Pop()
2020-10-28 15:49:49 -07:00
}
return nil
}
2020-10-28 15:49:49 -07:00
func (m *RenderSystem) updateWorld() error {
2020-10-28 15:49:49 -07:00
currentTime := time.Now()
elapsed := currentTime.Sub(m.lastUpdate)
m.lastUpdate = currentTime
2020-10-28 15:49:49 -07:00
return m.World.Update(elapsed)
}
func (m *RenderSystem) StartGameLoop() error {
changes to d2components, d2systems, d2ui, d2enum go.mod, go.sum: * updating akara, bugfix in akara.EntityManager.RemoveEntity d2core * adding d2core/d2label * adding d2core/d2bitmapfont d2ui * exporting some constants for use elsewhere d2components * added bitmap font component (for ui labels) * added FileLoaded tag component to simplify asset loading filters * added locale component * FilePath component renamed to File * sprite component now contains the sprite and palette path as strings * adding ui label component d2enum * added locale as file type for file "/data/local/use" d2systems * changed most info prints to debug prints * removed unused scene graph testing file (oops!) * terminal is now rendered above mouse cursor scene * adding ui widget system for use by the game object factory * adding test scene for ui labels created with the ui widget system d2systems/AppBootstrap * added command line args for profiler * `--testscene labels` launches the label test * now adds the local file for processing * game loop init logic now inside of Init method (the call to world.Update does this) d2systems/AssetLoader * loads the locale file and adds a locale component that other systems can use * adds a FileLoaded component after finished loading a file which other systems can use (like the loading scene) d2systems/FileSourceResolver * Now looks for and uses the locale for language/charset filepath substitution d2systems/GameClientBootstrap * game loop init moved to end of AppBootstrap.Init d2systems/GameObjectFactory * embedding UI widget factory system d2systems/BaseScene * made base scene a little more clear by breaking the process into more methods d2systems/LoadingScene * simplified the entity subscriptions by using the new FileLoaded component d2systems/SceneObjectFactory * adding method for adding labels, buttons to scenes (buttons still WIP) d2systems/SceneSpriteSystem * the sprite system now maintains a cache of rendered sprites
2020-12-08 10:42:19 -08:00
m.Info("starting game loop ...")
return m.renderer.Run(m.render, m.updateWorld, 800, 600, gameTitle)
}