package gg import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "surdeus.su/core/gods/maps" "time" "slices" ) import "surdeus.su/core/gg/mx" const ( MaxVertices = 1 << 16 ) type GraphicsLibrary = ebiten.GraphicsLibrary type RunOptions = ebiten.RunGameOptions // Window configuration type. type WindowConfig struct { DebugInfo ebiten.DebugInfo Options *RunOptions // The title of the window. Title string // Width and height of the window // in pixels. Width, Height int // Optional settings with // self describing names. FixedSize, Fullscreen, VSync bool } // The main structure that represents // current state of [game] engine. type Engine struct { wcfg WindowConfig // The main holder for objects. // Uses the map structure to quickly // delete and create new objects. objects *Objects // The main camera to display in window. // If is set to nil then the engine will panic. camera Camera drawLastTime time.Time drawdt Duration // Frame delta time. dt Duration lastTime time.Time // Temporary stuff keys, prevKeys []Key buttons MouseButtonMap wheel mx.Vector cursorPos mx.Vector outerEvents, handleEvents EventChan //bufs [LayerBufSize]*Image vertices map[Layer] []ebiten.Vertex //vindices []uint16 // Draw frame. dframe uint // Frame. frame uint runes []rune } type engine Engine // Get currently pressed keys. func (e *Engine) GetKeyboardKeys() []Key { return e.keys } func (e *Engine) GraphicsLibrary() GraphicsLibrary { return e.wcfg.DebugInfo.GraphicsLibrary } // Returns currently pressed buttons. func (e *Engine) GetMouseButtons() []MouseButton { ret := make([]MouseButton, len(e.buttons)) i := 0 for v := range e.buttons { ret[i] = v i++ } slices.Sort(ret) return ret } // Returns new empty Engine. func NewEngine( cfg WindowConfig, ) *Engine { ret := &Engine{} ret.wcfg = cfg ret.outerEvents = make(EventChan) ret.handleEvents = make(EventChan) ret.objects = NewObjects() ret.buttons = MouseButtonMap{} return ret } func (e *Engine) Camera() Camera { return e.camera } func (e *Engine) SetCamera(c Camera) *Engine { e.camera = c return e } func (e *Engine) EventInput() EventChan { return e.outerEvents } func (e *Engine) Exist(object Object) bool { return e.objects.has(object) } // Add new objects to the Engine's view. func (e *Engine) Spawn(object Object) bool { ctx := Context{ engine: e, } object.OnStart(ctx) ok := e.objects.add(object) return ok } // Delete object from Engine. func (e *Engine) Delete(object Object) bool { ctx := Context{ engine: e, } object.OnDelete(ctx) ok := e.objects.remove(object) return ok } var ( allButtons = []MouseButton{ MouseButton0, MouseButton1, MouseButton2, MouseButton3, MouseButton4, } ) func (e *Engine) IsPressed(k Key) bool { keys := e.GetKeyboardKeys() for _, v := range keys { if v == k { return true } } return false } func (e *Engine) IsButtoned(b MouseButton) bool { _, ok := e.buttons[b] return ok } func (e *Engine) GetMouseWheel() mx.Vector { return e.wheel } func (e *Engine) cursorPosition() mx.Vector { x, y := ebiten.CursorPosition() return mx.Vector{mx.Float(x), mx.Float(y)} } // Get the real cursor position. func (e *Engine) GetRealCursorPosition() mx.Vector { return e.cursorPos } // Get the absolute cursor position in the world // of the engine. func (e *Engine) GetAbsCursorPosition( ) mx.Vector { return e.GetRealCursorPosition(). Apply(e.camera.GetAbsMatrice(Context{ engine: e, })) } // Get the real window size in the current context. func (e *Engine) GetRealWinSize() mx.Vector { var w, h int if e.wcfg.Fullscreen { w, h = ebiten.ScreenSizeInFullscreen() } else { w, h = e.wcfg.Width, e.wcfg.Height } return mx.Vector{ mx.Float(w), mx.Float(h), } } func (e *Engine) GetAbsWinSize() mx.Vector { return e.camera.GetAbsWinSize(Context{ engine: e, }) } func (e *Engine) Runes() []rune { return e.runes } func (e *engine) updateEvents() Events { eng := (*Engine)(e) e.prevKeys = e.keys e.keys = inpututil. AppendPressedKeys(e.keys[:0]) events := Events{} // Mouse buttons. btns := e.buttons for _, btn := range allButtons { if inpututil.IsMouseButtonJustPressed(btn) { btns[btn] = struct{}{} events.Mouse.ButtonDowns = append( events.Mouse.ButtonDowns, MouseButtonDown{ MouseButton: btn, }, ) } else if inpututil.IsMouseButtonJustReleased(btn) { delete(btns, btn) events.Mouse.ButtonUps = append( events.Mouse.ButtonUps, MouseButtonUp{ MouseButton: btn, }, ) } } // Mouse wheel. x, y := ebiten.Wheel() e.wheel = mx.Vector{x, y} if !(e.wheel.Eq(mx.ZV)) { events.Mouse.WheelChange = &WheelChange{ Offset: e.wheel, } } // Cursor position. realPos := eng.cursorPosition() if !realPos.Eq(e.cursorPos) { absM := eng.camera.GetAbsMatrice(Context{ engine: eng, }) absPrevPos := e.cursorPos.Apply(absM) absPos := realPos.Apply(absM) events.Mouse.Move = &MouseMove{ RealDelta: realPos.Sub(e.cursorPos), AbsDelta: absPos.Sub(absPrevPos), } e.cursorPos = realPos } // Keyboard. keyDiff := diffEm(e.prevKeys, e.keys) for _, key := range keyDiff { if eng.IsPressed(key) { events.Keyboard.KeyDowns = append( events.Keyboard.KeyDowns, KeyDown{ Key: key, }, ) } else { events.Keyboard.KeyUps = append( events.Keyboard.KeyUps, KeyUp{ Key: key, }, ) } } return events } func (e *engine) Update() error { eng := (*Engine)(e) e.dt = time.Since(e.lastTime) e.runes = ebiten.AppendInputChars(e.runes[:0]) //fmt.Println("runes:", e.runes) // Buffering the context for faster. // Providing the events to the objects. // Maybe should think of the better way, // but for it is simple enough. events := e.updateEvents() c := Context{ engine: eng, events: events, } for _, object := range e.objects.store { if object == nil { continue } object.OnUpdate(c) } e.lastTime = time.Now() e.frame++ return nil } var ( fullPageIndexes = func() [MaxVertices]uint16 { ret := [MaxVertices]uint16{} for i:=0 ; i<len(ret) ; i++ { ret[i] = uint16(i) } return ret }() defaultPageImg = func() *Image { img := NewImage(1, 1) img.Set(0, 0, RGBA(1, 1, 1, 1)) return img }() defaultTriOptions = &ebiten.DrawTrianglesOptions{} ) func (e *engine) Draw(img *ebiten.Image) { e.drawdt = time.Since(e.drawLastTime) eng := (*Engine)(e) m := map[Layer][]Drawer{} for _, object := range eng.objects.store { // Skipping the ones we do not need to draw. if object == nil || !object.IsVisible() { continue } l := object.GetLayer() layer, ok := m[l] // Create new if has no the layer if !ok { m[l] = []Drawer{object} continue } m[l] = append(layer, object) } // Drawing layers via the sparse array. // First drawing via the inside function // and then the returned []EVertex. layers := maps.NewSparse[Layer, []Drawer](nil, m) c := Context{engine: eng, image: img} for layer := range layers.Chan() { vertices := Vertices{} for _, drawer := range layer { drawing := drawer.Draw(c) if drawing != nil { vertices = append( vertices, drawing.Vertices..., ) } } pn := len(vertices) / MaxVertices mod := len(vertices) % MaxVertices for i := 0 ; i<pn ; i++ { cur := i*MaxVertices img.DrawTriangles( vertices[cur:cur+MaxVertices].ToAPI(), fullPageIndexes[:], defaultPageImg, defaultTriOptions, ) } st := pn*MaxVertices img.DrawTriangles( vertices[st:].ToAPI(), fullPageIndexes[:mod], defaultPageImg, defaultTriOptions, ) } // Empty the buff to generate it again. e.drawLastTime = time.Now() e.dframe++ } func (e *engine) Layout(ow, oh int) (int, int) { if e.wcfg.FixedSize { return e.wcfg.Width, e.wcfg.Height } return ow, oh } // Return the delta time between Draw calls. func (e *Engine) DrawDT() Duration { return e.drawdt } // Current Drawing frame. func (e *Engine) Dframe() uint { return e.dframe } // Return the real delta time. // Please, prefer the DT(). func (e *Engine) RealDT() Duration { return e.dt } // Returns the current delta time. (btw frames) // (By the Ebiten community convention // currently it is a fixed value) func (e *Engine) DT() Duration { return time.Second/60 } // Current frame func (e *Engine) Frame() uint { return e.frame } // Current FPS. func (e *Engine) FPS() float64 { return ebiten.ActualFPS() } // Current TPS. func (e *Engine) TPS() float64 { return ebiten.ActualTPS() } // Run the engine. func (e *Engine) Run() error { ebiten.ReadDebugInfo(&e.wcfg.DebugInfo) ebiten.SetWindowTitle(e.wcfg.Title) ebiten.SetWindowSize(e.wcfg.Width, e.wcfg.Height) ebiten.SetWindowSizeLimits(1, 1, e.wcfg.Width, e.wcfg.Height) ebiten.SetFullscreen(e.wcfg.Fullscreen) ebiten.SetVsyncEnabled(e.wcfg.VSync) e.lastTime = time.Now() //fmt.Println(e.Objects) return ebiten.RunGameWithOptions((*engine)(e), e.wcfg.Options) } func (e *Engine) GetWindowConfig() WindowConfig { return e.wcfg }