mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 13:07:26 +01:00
7d56e4335e
This change introduces the new APIs RunGame, WindowSize and SetWindowSize. These new APIs hides the notion of 'scale', and is more flexible with the outside size change. This means that we can introduce a resizable window. This change also adds -legacy flag to examples/windowsize. If the flag is off, the new APIs are used. This change deprecates these functions since the notion of 'scale' is deprecated: * ScreenScale * ScreenSizeInFullscreen * SetScreenScale * SetScreenSize Fixes #943, #571 Updates #320
461 lines
10 KiB
Go
461 lines
10 KiB
Go
// Copyright 2016 Hajime Hoshi
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// +build android ios
|
|
|
|
package mobile
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"runtime/debug"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/mobile/app"
|
|
"golang.org/x/mobile/event/lifecycle"
|
|
"golang.org/x/mobile/event/paint"
|
|
"golang.org/x/mobile/event/size"
|
|
"golang.org/x/mobile/event/touch"
|
|
"golang.org/x/mobile/gl"
|
|
|
|
"github.com/hajimehoshi/ebiten/internal/devicescale"
|
|
"github.com/hajimehoshi/ebiten/internal/driver"
|
|
"github.com/hajimehoshi/ebiten/internal/graphicsdriver/opengl"
|
|
"github.com/hajimehoshi/ebiten/internal/hooks"
|
|
"github.com/hajimehoshi/ebiten/internal/thread"
|
|
)
|
|
|
|
var (
|
|
glContextCh = make(chan gl.Context, 1)
|
|
|
|
// renderCh receives when updating starts.
|
|
renderCh = make(chan struct{})
|
|
|
|
// renderEndCh receives when updating finishes.
|
|
renderEndCh = make(chan struct{})
|
|
|
|
theUI = &UserInterface{}
|
|
)
|
|
|
|
func init() {
|
|
theUI.input.ui = theUI
|
|
}
|
|
|
|
func Get() *UserInterface {
|
|
return theUI
|
|
}
|
|
|
|
func (u *UserInterface) Update() {
|
|
renderCh <- struct{}{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
<-renderEndCh
|
|
if u.t != nil {
|
|
u.t.Call(func() error {
|
|
cancel()
|
|
return nil
|
|
})
|
|
} else {
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
if u.graphics.IsGL() {
|
|
if u.glWorker == nil {
|
|
panic("mobile: glWorker must be initialized but not")
|
|
}
|
|
workAvailable := u.glWorker.WorkAvailable()
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-workAvailable:
|
|
u.glWorker.DoWork()
|
|
case <-ctx.Done():
|
|
break loop
|
|
}
|
|
}
|
|
return
|
|
} else {
|
|
u.t.Loop(ctx)
|
|
}
|
|
}
|
|
|
|
type UserInterface struct {
|
|
// TODO: Remove these members: the driver layer should not care about the game screen size.
|
|
width int
|
|
height int
|
|
scale float64
|
|
|
|
sizeChanged bool
|
|
|
|
// Used for gomobile-build
|
|
gbuildWidthPx int
|
|
gbuildHeightPx int
|
|
setGBuildSizeCh chan struct{}
|
|
|
|
context driver.UIContext
|
|
graphics driver.Graphics
|
|
|
|
input Input
|
|
|
|
t *thread.Thread
|
|
glWorker gl.Worker
|
|
|
|
m sync.RWMutex
|
|
}
|
|
|
|
func deviceScale() float64 {
|
|
return devicescale.GetAt(0, 0)
|
|
}
|
|
|
|
// appMain is the main routine for gomobile-build mode.
|
|
func (u *UserInterface) appMain(a app.App) {
|
|
var glctx gl.Context
|
|
var sizeInited bool
|
|
touches := map[touch.Sequence]*Touch{}
|
|
for e := range a.Events() {
|
|
switch e := a.Filter(e).(type) {
|
|
case lifecycle.Event:
|
|
switch e.Crosses(lifecycle.StageVisible) {
|
|
case lifecycle.CrossOn:
|
|
glctx, _ = e.DrawContext.(gl.Context)
|
|
// Assume that glctx is always a same instance.
|
|
// Then, only once initializing should be enough.
|
|
if glContextCh != nil {
|
|
glContextCh <- glctx
|
|
glContextCh = nil
|
|
}
|
|
a.Send(paint.Event{})
|
|
case lifecycle.CrossOff:
|
|
glctx = nil
|
|
}
|
|
case size.Event:
|
|
u.setGBuildSize(e.WidthPx, e.HeightPx)
|
|
sizeInited = true
|
|
case paint.Event:
|
|
if !sizeInited {
|
|
a.Send(paint.Event{})
|
|
continue
|
|
}
|
|
if glctx == nil || e.External {
|
|
continue
|
|
}
|
|
renderCh <- struct{}{}
|
|
<-renderEndCh
|
|
a.Publish()
|
|
a.Send(paint.Event{})
|
|
case touch.Event:
|
|
if !sizeInited {
|
|
continue
|
|
}
|
|
switch e.Type {
|
|
case touch.TypeBegin, touch.TypeMove:
|
|
s := deviceScale()
|
|
x, y := float64(e.X)/s, float64(e.Y)/s
|
|
// TODO: Is it ok to cast from int64 to int here?
|
|
touches[e.Sequence] = &Touch{
|
|
ID: int(e.Sequence),
|
|
X: int(x),
|
|
Y: int(y),
|
|
}
|
|
case touch.TypeEnd:
|
|
delete(touches, e.Sequence)
|
|
}
|
|
ts := []*Touch{}
|
|
for _, t := range touches {
|
|
ts = append(ts, t)
|
|
}
|
|
u.input.update(ts)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (u *UserInterface) Run(context driver.UIContext, graphics driver.Graphics) error {
|
|
// TODO: Remove width/height/scale arguments. They are not used from gomobile-build.
|
|
|
|
u.setGBuildSizeCh = make(chan struct{})
|
|
go func() {
|
|
if err := u.run(16, 16, 1, context, graphics, true); err != nil {
|
|
// As mobile apps never ends, Loop can't return. Just panic here.
|
|
panic(err)
|
|
}
|
|
}()
|
|
app.Main(u.appMain)
|
|
return nil
|
|
}
|
|
|
|
func (u *UserInterface) RunWithoutMainLoop(width, height int, scale float64, title string, context driver.UIContext, graphics driver.Graphics) <-chan error {
|
|
ch := make(chan error)
|
|
go func() {
|
|
defer close(ch)
|
|
// title is ignored?
|
|
if err := u.run(width, height, scale, context, graphics, false); err != nil {
|
|
ch <- err
|
|
}
|
|
}()
|
|
|
|
return ch
|
|
}
|
|
|
|
func (u *UserInterface) run(width, height int, scale float64, context driver.UIContext, graphics driver.Graphics, mainloop bool) (err error) {
|
|
// Convert the panic to a regular error so that Java/Objective-C layer can treat this easily e.g., for
|
|
// Crashlytics. A panic is treated as SIGABRT, and there is no way to handle this on Java/Objective-C layer
|
|
// unfortunately.
|
|
// TODO: Panic on other goroutines cannot be handled here.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("%v\n%q", r, string(debug.Stack()))
|
|
}
|
|
}()
|
|
|
|
u.m.Lock()
|
|
u.width = width
|
|
u.height = height
|
|
u.scale = scale
|
|
u.sizeChanged = true
|
|
u.context = context
|
|
u.graphics = graphics
|
|
u.m.Unlock()
|
|
|
|
if graphics.IsGL() {
|
|
var ctx gl.Context
|
|
if mainloop {
|
|
ctx = <-glContextCh
|
|
} else {
|
|
ctx, u.glWorker = gl.NewContext()
|
|
}
|
|
graphics.(*opengl.Driver).SetMobileGLContext(ctx)
|
|
} else {
|
|
u.t = thread.New()
|
|
graphics.SetThread(u.t)
|
|
}
|
|
|
|
// If gomobile-build is used, wait for the outside size fixed.
|
|
if u.setGBuildSizeCh != nil {
|
|
<-u.setGBuildSizeCh
|
|
}
|
|
|
|
// Force to set the screen size
|
|
u.updateSize(context)
|
|
for {
|
|
if err := u.update(context); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (u *UserInterface) updateSize(context driver.UIContext) {
|
|
var width, height float64
|
|
|
|
u.m.Lock()
|
|
sizeChanged := u.sizeChanged
|
|
if sizeChanged {
|
|
if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 {
|
|
s := u.scale
|
|
width = float64(u.width) * s
|
|
height = float64(u.height) * s
|
|
} else {
|
|
// gomobile build
|
|
d := deviceScale()
|
|
width = float64(u.gbuildWidthPx) / d
|
|
height = float64(u.gbuildHeightPx) / d
|
|
}
|
|
}
|
|
u.sizeChanged = false
|
|
u.m.Unlock()
|
|
|
|
if sizeChanged {
|
|
// Dirty hack to set the offscreen size for gomobile-bind.
|
|
// TODO: Remove this. The layouting logic must be in the package ebiten, not here.
|
|
if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 {
|
|
context.(interface {
|
|
SetScreenSize(width, height int)
|
|
}).SetScreenSize(u.width, u.height)
|
|
}
|
|
|
|
// Sizing also calls GL functions
|
|
context.Layout(width, height)
|
|
}
|
|
}
|
|
|
|
func (u *UserInterface) update(context driver.UIContext) error {
|
|
t := time.NewTimer(500 * time.Millisecond)
|
|
defer t.Stop()
|
|
|
|
select {
|
|
case <-renderCh:
|
|
case <-t.C:
|
|
hooks.SuspendAudio()
|
|
<-renderCh
|
|
}
|
|
|
|
hooks.ResumeAudio()
|
|
|
|
defer func() {
|
|
renderEndCh <- struct{}{}
|
|
}()
|
|
|
|
if err := context.Update(func() {
|
|
u.updateSize(context)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
|
|
// TODO: This function should return gbuildWidthPx, gbuildHeightPx,
|
|
// but these values are not initialized until the main loop starts.
|
|
return 0, 0
|
|
}
|
|
|
|
func (u *UserInterface) SetScreenSizeAndScale(width, height int, scale float64) {
|
|
// Called from ebitenmobileview.
|
|
u.m.Lock()
|
|
if u.width != width || u.height != height || u.scale != scale {
|
|
u.width = width
|
|
u.height = height
|
|
u.scale = scale
|
|
u.sizeChanged = true
|
|
}
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) setGBuildSize(widthPx, heightPx int) {
|
|
u.m.Lock()
|
|
u.gbuildWidthPx = widthPx
|
|
u.gbuildHeightPx = heightPx
|
|
u.sizeChanged = true
|
|
close(u.setGBuildSizeCh)
|
|
u.m.Unlock()
|
|
}
|
|
|
|
func (u *UserInterface) adjustPosition(x, y int) (int, int) {
|
|
// This function's caller already protects this function by the mutex.
|
|
if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 {
|
|
s := u.scale
|
|
return int(float64(x) / s), int(float64(y) / s)
|
|
}
|
|
xf, yf := u.context.AdjustPosition(float64(x), float64(y))
|
|
return int(xf), int(yf)
|
|
}
|
|
|
|
func (u *UserInterface) CursorMode() driver.CursorMode {
|
|
return driver.CursorModeHidden
|
|
}
|
|
|
|
func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) IsFullscreen() bool {
|
|
return false
|
|
}
|
|
|
|
func (u *UserInterface) SetFullscreen(fullscreen bool) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) IsRunnableInBackground() bool {
|
|
return false
|
|
}
|
|
|
|
func (u *UserInterface) SetRunnableInBackground(runnableInBackground bool) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowTitle(title string) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowIcon(iconImages []image.Image) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) IsWindowDecorated() bool {
|
|
return false
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowDecorated(decorated bool) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) IsWindowResizable() bool {
|
|
return false
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowResizable(decorated bool) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) IsVsyncEnabled() bool {
|
|
return true
|
|
}
|
|
|
|
func (u *UserInterface) SetVsyncEnabled(enabled bool) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) DeviceScaleFactor() float64 {
|
|
return deviceScale()
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowPosition(x, y int) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) WindowPosition() (int, int) {
|
|
return 0, 0
|
|
}
|
|
|
|
func (u *UserInterface) SetScreenTransparent(transparent bool) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) IsScreenTransparent() bool {
|
|
return false
|
|
}
|
|
|
|
func (u *UserInterface) SetWindowSize(width, height int) {
|
|
// Do nothing
|
|
}
|
|
|
|
func (u *UserInterface) WindowSize() (int, int) {
|
|
return 0, 0
|
|
}
|
|
|
|
func (u *UserInterface) CanHaveWindow() bool {
|
|
return false
|
|
}
|
|
|
|
func (u *UserInterface) MonitorPosition() (int, int) {
|
|
return 0, 0
|
|
}
|
|
|
|
func (u *UserInterface) Input() driver.Input {
|
|
return &u.input
|
|
}
|
|
|
|
type Touch struct {
|
|
ID int
|
|
X int
|
|
Y int
|
|
}
|
|
|
|
func (u *UserInterface) UpdateInput(touches []*Touch) {
|
|
u.input.update(touches)
|
|
}
|