mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-24 18:58:54 +01:00
internal/ui: refactoring: move the logic in gameForUI to context
This commit is contained in:
parent
9c448d207a
commit
5c7917897c
91
gameforui.go
91
gameforui.go
@ -15,16 +15,12 @@
|
||||
package ebiten
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
||||
)
|
||||
|
||||
type gameForUI struct {
|
||||
game Game
|
||||
offscreen *Image
|
||||
screen *Image
|
||||
}
|
||||
|
||||
func newGameForUI(game Game) *gameForUI {
|
||||
@ -33,90 +29,23 @@ func newGameForUI(game Game) *gameForUI {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gameForUI) Layout(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
|
||||
ow, oh := c.game.Layout(int(outsideWidth), int(outsideHeight))
|
||||
if ow <= 0 || oh <= 0 {
|
||||
panic("ebiten: Layout must return positive numbers")
|
||||
}
|
||||
|
||||
sw, sh := int(outsideWidth*deviceScaleFactor), int(outsideHeight*deviceScaleFactor)
|
||||
if c.screen != nil {
|
||||
if w, h := c.screen.Size(); w != sw || h != sh {
|
||||
c.screen.Dispose()
|
||||
c.screen = nil
|
||||
}
|
||||
}
|
||||
if c.screen == nil {
|
||||
c.screen = newScreenFramebufferImage(sw, sh)
|
||||
}
|
||||
|
||||
func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
|
||||
if c.offscreen != nil {
|
||||
if w, h := c.offscreen.Size(); w != ow || h != oh {
|
||||
c.offscreen.Dispose()
|
||||
c.offscreen = nil
|
||||
}
|
||||
c.offscreen.Dispose()
|
||||
c.offscreen = nil
|
||||
}
|
||||
if c.offscreen == nil {
|
||||
c.offscreen = NewImage(ow, oh)
|
||||
c.offscreen = NewImage(width, height)
|
||||
return c.offscreen.image
|
||||
}
|
||||
|
||||
// Keep the offscreen an independent image from an atlas (#1938).
|
||||
// The shader program for the screen is special and doesn't work well with an image on an atlas.
|
||||
// An image on an atlas is surrounded by a transparent edge,
|
||||
// and the shader program unexpectedly picks the pixel on the edges.
|
||||
c.offscreen.image.SetIndependent(true)
|
||||
}
|
||||
|
||||
return ow, oh
|
||||
func (c *gameForUI) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||
return c.game.Layout(outsideWidth, outsideHeight)
|
||||
}
|
||||
|
||||
func (c *gameForUI) Update() error {
|
||||
return c.game.Update()
|
||||
}
|
||||
|
||||
func (c *gameForUI) Draw(screenScale float64, offsetX, offsetY float64, needsClearingScreen bool, framebufferYDirection graphicsdriver.YDirection, clearScreenEveryFrame, filterEnabled bool) {
|
||||
c.offscreen.image.SetVolatile(clearScreenEveryFrame)
|
||||
|
||||
// Even though updateCount == 0, the offscreen is cleared and Draw is called.
|
||||
// Draw should not update the game state and then the screen should not be updated without Update, but
|
||||
// users might want to process something at Draw with the time intervals of FPS.
|
||||
if clearScreenEveryFrame {
|
||||
c.offscreen.Clear()
|
||||
}
|
||||
func (c *gameForUI) Draw() {
|
||||
c.game.Draw(c.offscreen)
|
||||
|
||||
if needsClearingScreen {
|
||||
// This clear is needed for fullscreen mode or some mobile platforms (#622).
|
||||
c.screen.Clear()
|
||||
}
|
||||
|
||||
op := &DrawImageOptions{}
|
||||
|
||||
s := screenScale
|
||||
switch framebufferYDirection {
|
||||
case graphicsdriver.Upward:
|
||||
op.GeoM.Scale(s, -s)
|
||||
_, h := c.offscreen.Size()
|
||||
op.GeoM.Translate(0, float64(h)*s)
|
||||
case graphicsdriver.Downward:
|
||||
op.GeoM.Scale(s, s)
|
||||
default:
|
||||
panic(fmt.Sprintf("ebiten: invalid v-direction: %d", framebufferYDirection))
|
||||
}
|
||||
|
||||
op.GeoM.Translate(offsetX, offsetY)
|
||||
op.CompositeMode = CompositeModeCopy
|
||||
|
||||
switch {
|
||||
case !filterEnabled:
|
||||
op.Filter = FilterNearest
|
||||
case math.Floor(s) == s:
|
||||
op.Filter = FilterNearest
|
||||
case s > 1:
|
||||
op.Filter = filterScreen
|
||||
default:
|
||||
// filterScreen works with >=1 scale, but does not well with <1 scale.
|
||||
// Use regular FilterLinear instead so far (#669).
|
||||
op.Filter = FilterLinear
|
||||
}
|
||||
c.screen.DrawImage(c.offscreen, op)
|
||||
}
|
||||
|
@ -27,11 +27,6 @@ const (
|
||||
|
||||
// FilterLinear represents linear filter
|
||||
FilterLinear Filter = Filter(graphicsdriver.FilterLinear)
|
||||
|
||||
// filterScreen represents a special filter for screen. Inner usage only.
|
||||
//
|
||||
// Some parameters like a color matrix or color vertex values can be ignored when filterScreen is used.
|
||||
filterScreen Filter = Filter(graphicsdriver.FilterScreen)
|
||||
)
|
||||
|
||||
// CompositeMode represents Porter-Duff composition mode.
|
||||
|
48
image.go
48
image.go
@ -66,39 +66,22 @@ func (i *Image) Clear() {
|
||||
i.Fill(color.Transparent)
|
||||
}
|
||||
|
||||
var (
|
||||
emptyImage = NewImage(3, 3)
|
||||
emptySubImage = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*Image)
|
||||
)
|
||||
|
||||
func init() {
|
||||
w, h := emptyImage.Size()
|
||||
pix := make([]byte, 4*w*h)
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
// As emptyImage is used at Fill, use ReplacePixels instead.
|
||||
emptyImage.ReplacePixels(pix)
|
||||
}
|
||||
|
||||
// Fill fills the image with a solid color.
|
||||
//
|
||||
// When the image is disposed, Fill does nothing.
|
||||
func (i *Image) Fill(clr color.Color) {
|
||||
// Use the original size to cover the entire region (#1691).
|
||||
// DrawImage automatically clips the rendering region.
|
||||
orig := i
|
||||
if i.isSubImage() {
|
||||
orig = i.original
|
||||
i.copyCheck()
|
||||
|
||||
var crf, cgf, cbf, caf float32
|
||||
cr, cg, cb, ca := clr.RGBA()
|
||||
if ca != 0 {
|
||||
crf = float32(cr) / float32(ca)
|
||||
cgf = float32(cg) / float32(ca)
|
||||
cbf = float32(cb) / float32(ca)
|
||||
caf = float32(ca) / 0xffff
|
||||
}
|
||||
w, h := orig.Size()
|
||||
|
||||
op := &DrawImageOptions{}
|
||||
op.GeoM.Scale(float64(w), float64(h))
|
||||
op.ColorM.ScaleWithColor(clr)
|
||||
op.CompositeMode = CompositeModeCopy
|
||||
|
||||
i.DrawImage(emptySubImage, op)
|
||||
b := i.Bounds()
|
||||
i.image.Fill(crf, cgf, cbf, caf, b.Min.X, b.Min.Y, b.Dx(), b.Dy())
|
||||
}
|
||||
|
||||
func canSkipMipmap(geom GeoM, filter graphicsdriver.Filter) bool {
|
||||
@ -845,12 +828,3 @@ func NewImageFromImage(source image.Image) *Image {
|
||||
i.ReplacePixels(imageToBytes(source))
|
||||
return i
|
||||
}
|
||||
|
||||
func newScreenFramebufferImage(width, height int) *Image {
|
||||
i := &Image{
|
||||
image: ui.NewScreenFramebufferImage(width, height),
|
||||
bounds: image.Rect(0, 0, width, height),
|
||||
}
|
||||
i.addr = i
|
||||
return i
|
||||
}
|
||||
|
@ -15,10 +15,12 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/affine"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/clock"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/debug"
|
||||
@ -30,9 +32,10 @@ import (
|
||||
const DefaultTPS = 60
|
||||
|
||||
type Game interface {
|
||||
Layout(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int)
|
||||
NewOffscreenImage(width, height int) *Image
|
||||
Layout(outsideWidth, outsideHeight int) (int, int)
|
||||
Update() error
|
||||
Draw(screenScale float64, offsetX, offsetY float64, needsClearingScreen bool, framebufferYDirection graphicsdriver.YDirection, screenClearedEveryFrame, filterEnabled bool)
|
||||
Draw()
|
||||
}
|
||||
|
||||
type context struct {
|
||||
@ -40,11 +43,12 @@ type context struct {
|
||||
|
||||
updateCalled bool
|
||||
|
||||
offscreen *Image
|
||||
screen *Image
|
||||
|
||||
// The following members must be protected by the mutex m.
|
||||
outsideWidth float64
|
||||
outsideHeight float64
|
||||
screenWidth int
|
||||
screenHeight int
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
@ -116,12 +120,11 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
|
||||
if err := theGlobalState.error(); err != nil {
|
||||
return err
|
||||
}
|
||||
Get().resetForTick()
|
||||
theUI.resetForTick()
|
||||
}
|
||||
|
||||
// Draw the game.
|
||||
screenScale, offsetX, offsetY := c.screenScaleAndOffsets(deviceScaleFactor)
|
||||
c.game.Draw(screenScale, offsetX, offsetY, graphicsDriver.NeedsClearingScreen(), graphicsDriver.FramebufferYDirection(), theGlobalState.isScreenClearedEveryFrame(), theGlobalState.isScreenFilterEnabled())
|
||||
c.drawGame(graphicsDriver)
|
||||
|
||||
// All the vertices data are consumed at the end of the frame, and the data backend can be
|
||||
// available after that. Until then, lock the vertices backend.
|
||||
@ -133,20 +136,119 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
|
||||
})
|
||||
}
|
||||
|
||||
func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
|
||||
c.offscreen.mipmap.SetVolatile(theGlobalState.isScreenClearedEveryFrame())
|
||||
|
||||
// Even though updateCount == 0, the offscreen is cleared and Draw is called.
|
||||
// Draw should not update the game state and then the screen should not be updated without Update, but
|
||||
// users might want to process something at Draw with the time intervals of FPS.
|
||||
if theGlobalState.isScreenClearedEveryFrame() {
|
||||
c.offscreen.clear()
|
||||
}
|
||||
c.game.Draw()
|
||||
|
||||
if graphicsDriver.NeedsClearingScreen() {
|
||||
// This clear is needed for fullscreen mode or some mobile platforms (#622).
|
||||
c.screen.clear()
|
||||
}
|
||||
|
||||
ga := 1.0
|
||||
gd := 1.0
|
||||
gtx := 0.0
|
||||
gty := 0.0
|
||||
|
||||
screenScale, offsetX, offsetY := c.screenScaleAndOffsets()
|
||||
s := screenScale
|
||||
switch y := graphicsDriver.FramebufferYDirection(); y {
|
||||
case graphicsdriver.Upward:
|
||||
ga *= s
|
||||
gd *= -s
|
||||
gty += float64(c.offscreen.height) * s
|
||||
case graphicsdriver.Downward:
|
||||
ga *= s
|
||||
gd *= s
|
||||
default:
|
||||
panic(fmt.Sprintf("ui: invalid y-direction: %d", y))
|
||||
}
|
||||
|
||||
gtx += offsetX
|
||||
gty += offsetY
|
||||
|
||||
var filter graphicsdriver.Filter
|
||||
switch {
|
||||
case !theGlobalState.isScreenFilterEnabled():
|
||||
filter = graphicsdriver.FilterNearest
|
||||
case math.Floor(s) == s:
|
||||
filter = graphicsdriver.FilterNearest
|
||||
case s > 1:
|
||||
filter = graphicsdriver.FilterScreen
|
||||
default:
|
||||
// FilterScreen works with >=1 scale, but does not well with <1 scale.
|
||||
// Use regular FilterLinear instead so far (#669).
|
||||
filter = graphicsdriver.FilterLinear
|
||||
}
|
||||
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
Width: float32(c.screen.width),
|
||||
Height: float32(c.screen.height),
|
||||
}
|
||||
|
||||
vs := graphics.QuadVertices(
|
||||
0, 0, float32(c.offscreen.width), float32(c.offscreen.height),
|
||||
float32(ga), 0, 0, float32(gd), float32(gtx), float32(gty),
|
||||
1, 1, 1, 1)
|
||||
is := graphics.QuadIndices()
|
||||
|
||||
srcs := [graphics.ShaderImageNum]*Image{c.offscreen}
|
||||
c.screen.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, filter, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, true)
|
||||
}
|
||||
|
||||
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
|
||||
c.outsideWidth = outsideWidth
|
||||
c.outsideHeight = outsideHeight
|
||||
w, h := c.game.Layout(outsideWidth, outsideHeight, deviceScaleFactor)
|
||||
c.screenWidth = w
|
||||
c.screenHeight = h
|
||||
return w, h
|
||||
|
||||
ow, oh := c.game.Layout(int(outsideWidth), int(outsideHeight))
|
||||
if ow <= 0 || oh <= 0 {
|
||||
panic("ui: Layout must return positive numbers")
|
||||
}
|
||||
|
||||
sw, sh := int(outsideWidth*deviceScaleFactor), int(outsideHeight*deviceScaleFactor)
|
||||
if c.screen != nil {
|
||||
if c.screen.width != sw || c.screen.height != sh {
|
||||
c.screen.MarkDisposed()
|
||||
c.screen = nil
|
||||
}
|
||||
}
|
||||
if c.screen == nil {
|
||||
c.screen = newScreenFramebufferImage(sw, sh)
|
||||
}
|
||||
|
||||
if c.offscreen != nil {
|
||||
if c.offscreen.width != ow || c.offscreen.height != oh {
|
||||
c.offscreen.MarkDisposed()
|
||||
c.offscreen = nil
|
||||
}
|
||||
}
|
||||
if c.offscreen == nil {
|
||||
c.offscreen = c.game.NewOffscreenImage(ow, oh)
|
||||
|
||||
// Keep the offscreen an independent image from an atlas (#1938).
|
||||
// The shader program for the screen is special and doesn't work well with an image on an atlas.
|
||||
// An image on an atlas is surrounded by a transparent edge,
|
||||
// and the shader program unexpectedly picks the pixel on the edges.
|
||||
c.offscreen.mipmap.SetIndependent(true)
|
||||
}
|
||||
|
||||
return ow, oh
|
||||
}
|
||||
|
||||
func (c *context) adjustPosition(x, y float64, deviceScaleFactor float64) (float64, float64) {
|
||||
s, ox, oy := c.screenScaleAndOffsets(deviceScaleFactor)
|
||||
s, ox, oy := c.screenScaleAndOffsets()
|
||||
// The scale 0 indicates that the screen is not initialized yet.
|
||||
// As any cursor values don't make sense, just return NaN.
|
||||
if s == 0 {
|
||||
@ -155,21 +257,21 @@ func (c *context) adjustPosition(x, y float64, deviceScaleFactor float64) (float
|
||||
return (x*deviceScaleFactor - ox) / s, (y*deviceScaleFactor - oy) / s
|
||||
}
|
||||
|
||||
func (c *context) screenScaleAndOffsets(deviceScaleFactor float64) (float64, float64, float64) {
|
||||
func (c *context) screenScaleAndOffsets() (float64, float64, float64) {
|
||||
c.m.Lock()
|
||||
defer c.m.Unlock()
|
||||
|
||||
if c.screenWidth == 0 || c.screenHeight == 0 {
|
||||
if c.screen == nil {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
scaleX := c.outsideWidth / float64(c.screenWidth) * deviceScaleFactor
|
||||
scaleY := c.outsideHeight / float64(c.screenHeight) * deviceScaleFactor
|
||||
scaleX := float64(c.screen.width) / float64(c.offscreen.width)
|
||||
scaleY := float64(c.screen.height) / float64(c.offscreen.height)
|
||||
scale := math.Min(scaleX, scaleY)
|
||||
width := float64(c.screenWidth) * scale
|
||||
height := float64(c.screenHeight) * scale
|
||||
x := (c.outsideWidth*deviceScaleFactor - width) / 2
|
||||
y := (c.outsideHeight*deviceScaleFactor - height) / 2
|
||||
width := float64(c.offscreen.width) * scale
|
||||
height := float64(c.offscreen.height) * scale
|
||||
x := (float64(c.screen.width) - width) / 2
|
||||
y := (float64(c.screen.height) - height) / 2
|
||||
return scale, x, y
|
||||
}
|
||||
|
||||
@ -257,7 +359,7 @@ func FPSMode() FPSModeType {
|
||||
|
||||
func SetFPSMode(fpsMode FPSModeType) {
|
||||
theGlobalState.setFPSMode(fpsMode)
|
||||
Get().SetFPSMode(fpsMode)
|
||||
theUI.SetFPSMode(fpsMode)
|
||||
}
|
||||
|
||||
func MaxTPS() int {
|
||||
|
@ -31,17 +31,23 @@ func SetPanicOnErrorOnReadingPixelsForTesting(value bool) {
|
||||
|
||||
type Image struct {
|
||||
mipmap *mipmap.Mipmap
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func NewImage(width, height int) *Image {
|
||||
return &Image{
|
||||
mipmap: mipmap.New(width, height),
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
func NewScreenFramebufferImage(width, height int) *Image {
|
||||
func newScreenFramebufferImage(width, height int) *Image {
|
||||
return &Image{
|
||||
mipmap: mipmap.NewScreenFramebufferMipmap(width, height),
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,14 +98,42 @@ func (i *Image) DumpScreenshot(name string, blackbg bool) error {
|
||||
return theUI.dumpScreenshot(i.mipmap, name, blackbg)
|
||||
}
|
||||
|
||||
func (i *Image) SetIndependent(independent bool) {
|
||||
i.mipmap.SetIndependent(independent)
|
||||
}
|
||||
|
||||
func (i *Image) SetVolatile(volatile bool) {
|
||||
i.mipmap.SetVolatile(volatile)
|
||||
}
|
||||
|
||||
func DumpImages(dir string) error {
|
||||
return theUI.dumpImages(dir)
|
||||
}
|
||||
|
||||
var (
|
||||
emptyImage = NewImage(3, 3)
|
||||
)
|
||||
|
||||
func init() {
|
||||
pix := make([]byte, 4*emptyImage.width*emptyImage.height)
|
||||
for i := range pix {
|
||||
pix[i] = 0xff
|
||||
}
|
||||
// As emptyImage is used at Fill, use ReplacePixels instead.
|
||||
emptyImage.ReplacePixels(pix, 0, 0, emptyImage.width, emptyImage.height)
|
||||
}
|
||||
|
||||
func (i *Image) clear() {
|
||||
i.Fill(0, 0, 0, 0, 0, 0, i.width, i.height)
|
||||
}
|
||||
|
||||
func (i *Image) Fill(r, g, b, a float32, x, y, width, height int) {
|
||||
dstRegion := graphicsdriver.Region{
|
||||
X: float32(x),
|
||||
Y: float32(y),
|
||||
Width: float32(width),
|
||||
Height: float32(height),
|
||||
}
|
||||
|
||||
vs := graphics.QuadVertices(
|
||||
1, 1, float32(emptyImage.width-1), float32(emptyImage.height-1),
|
||||
float32(i.width), 0, 0, float32(i.height), 0, 0,
|
||||
r, g, b, a)
|
||||
is := graphics.QuadIndices()
|
||||
|
||||
srcs := [graphics.ShaderImageNum]*Image{emptyImage}
|
||||
|
||||
i.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, true)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user