├── .github └── workflows │ └── scala.yml ├── .gitignore ├── BUGS.md ├── LICENSE ├── README.md ├── TODO ├── android ├── README.md ├── admob │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── scala │ │ └── sgl │ │ └── android │ │ └── ads │ │ └── AndroidAdMobProvider.scala ├── build.sbt ├── core │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── scala │ │ └── sgl │ │ └── android │ │ ├── AndroidApp.scala │ │ ├── AndroidAudioProvider.scala │ │ ├── AndroidGraphicsProvider.scala │ │ ├── AndroidInputProvider.scala │ │ ├── AndroidOpenGLGraphicsProvider.scala │ │ ├── AndroidSave.scala │ │ ├── AndroidSystemProvider.scala │ │ ├── AndroidWindowProvider.scala │ │ ├── services │ │ └── package.scala │ │ └── util │ │ ├── AndroidJsonProvider.scala │ │ └── AndroidLoggingProvider.scala ├── firebase │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── scala │ │ └── sgl │ │ └── android │ │ └── analytics │ │ └── AndroidFirebaseAnalyticsProvider.scala ├── google-analytics │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── scala │ │ └── sgl │ │ └── android │ │ └── analytics │ │ └── AndroidGAAnalyticsProvider.scala ├── google-play │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── scala │ │ └── sgl │ │ └── android │ │ └── services │ │ └── GoogleGamesServices.scala └── project │ ├── build.properties │ └── plugins.sbt ├── build.sbt ├── core └── src │ ├── main │ └── scala │ │ └── sgl │ │ ├── AudioProvider.scala │ │ ├── Camera.scala │ │ ├── GameApp.scala │ │ ├── GameLoopComponent.scala │ │ ├── GameLoopStatisticsComponent.scala │ │ ├── GameStateComponent.scala │ │ ├── GraphicsHelpers.scala │ │ ├── GraphicsProvider.scala │ │ ├── Input.scala │ │ ├── InputProcessor.scala │ │ ├── LifecycleListener.scala │ │ ├── ParticleSystemComponent.scala │ │ ├── Save.scala │ │ ├── SystemProvider.scala │ │ ├── Viewport.scala │ │ ├── WindowProvider.scala │ │ ├── achievements │ │ └── Achievements.scala │ │ ├── ads │ │ └── AdsProvider.scala │ │ ├── analytics │ │ ├── AnalyticsProvider.scala │ │ ├── GameStateAutoAnalyticsComponent.scala │ │ ├── LoggedAnalyticsProvider.scala │ │ └── NoAnalyticsProvider.scala │ │ ├── geometry │ │ ├── Circle.scala │ │ ├── Collisions.scala │ │ ├── Line.scala │ │ ├── Point.scala │ │ ├── Polygon.scala │ │ ├── Rect.scala │ │ ├── Vec.scala │ │ └── package.scala │ │ ├── package.scala │ │ ├── proxy │ │ ├── PlatformProxy.scala │ │ ├── ProxiedGameApp.scala │ │ ├── ProxyGraphicsProvider.scala │ │ ├── ProxyPlatformProvider.scala │ │ ├── ProxySchedulerProvider.scala │ │ ├── ProxySystemProvider.scala │ │ └── ProxyWindowProvider.scala │ │ ├── scene │ │ ├── Action.scala │ │ ├── Scene.scala │ │ ├── SceneGraph.scala │ │ ├── package.scala │ │ └── ui │ │ │ ├── Buttons.scala │ │ │ ├── Popup.scala │ │ │ ├── ScrollPane.scala │ │ │ └── Widget.scala │ │ ├── tiled │ │ ├── TiledMap.scala │ │ ├── TiledMapRenderer.scala │ │ └── TmxJsonParser.scala │ │ └── util │ │ ├── AssertionsProvider.scala │ │ ├── ChunkedTask.scala │ │ ├── JsonProvider.scala │ │ ├── Loader.scala │ │ ├── LoggingProvider.scala │ │ ├── Math.scala │ │ ├── Pool.scala │ │ ├── RandomProvider.scala │ │ ├── SchedulerProvider.scala │ │ ├── TweeningEquations.scala │ │ └── metrics │ │ ├── Counter.scala │ │ ├── Gauge.scala │ │ ├── Histogram.scala │ │ ├── InstrumentationProvider.scala │ │ └── Metrics.scala │ └── test │ └── scala │ └── sgl │ ├── GraphicsHelpersSuite.scala │ ├── SaveComponentSuite.scala │ ├── SystemProviderSuite.scala │ ├── TestGraphicsProvider.scala │ ├── TestSystemProvider.scala │ ├── ViewportSuite.scala │ ├── geometry │ ├── CircleSuite.scala │ ├── CollisionsSuite.scala │ ├── EllipseSuite.scala │ ├── PolygonSuite.scala │ └── RectSuite.scala │ └── util │ ├── DefaultLoaderSuite.scala │ ├── JsonProviderAbstractSuite.scala │ ├── LoaderAbstractSuite.scala │ ├── RandomProviderSuite.scala │ ├── TweeningEquationsSuite.scala │ └── metrics │ └── MetricsSuite.scala ├── desktop-awt └── src │ ├── main │ └── scala │ │ └── sgl │ │ └── awt │ │ ├── AWTApp.scala │ │ ├── AWTAudioProvider.scala │ │ ├── AWTGraphicsProvider.scala │ │ ├── AWTInputProvider.scala │ │ ├── AWTSystemProvider.scala │ │ ├── AWTWindowProvider.scala │ │ ├── FileSave.scala │ │ └── util │ │ ├── LiftJsonProvider.scala │ │ └── TerminalLoggingProvider.scala │ └── test │ └── scala │ └── sgl │ └── awt │ └── util │ └── LiftJsonProviderSuite.scala ├── desktop-native └── src │ └── main │ └── scala │ └── sgl │ └── native │ ├── NativeApp.scala │ ├── NativeAudioProvider.scala │ ├── NativeGraphicsProvider.scala │ ├── NativeInputProvider.scala │ ├── NativeSystemProvider.scala │ ├── NativeWindowProvider.scala │ └── util │ └── TerminalLoggingProvider.scala ├── doc ├── 2020-06-07-cross-platform-game-development-in-scala-natively.md └── 2020-06-21-making-a-scala-game-part2.md ├── examples ├── .DS_Store ├── board │ ├── README.md │ ├── core │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ ├── App.scala │ │ │ └── MainScreen.scala │ ├── desktop-awt │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ ├── desktop-native │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ └── html5 │ │ ├── index.html │ │ └── src │ │ └── main │ │ └── scala │ │ └── Main.scala ├── hello │ ├── .DS_Store │ ├── README.md │ ├── android │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── res │ │ │ └── drawable-mdpi │ │ │ │ └── character.png │ │ │ └── scala │ │ │ └── MainActivity.scala │ ├── assets │ │ ├── audio │ │ │ ├── beep.wav │ │ │ ├── music.ogg │ │ │ └── music.wav │ │ ├── drawable-mdpi │ │ │ └── character.png │ │ └── drawable-xhdpi │ │ │ └── character.png │ ├── core │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ ├── App.scala │ │ │ └── MainScreen.scala │ ├── desktop-awt │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ ├── desktop-native │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ └── html5 │ │ ├── index.html │ │ ├── src │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ │ └── static │ │ ├── audio │ │ ├── drawable-mdpi │ │ └── drawable-xhdpi ├── menu │ ├── README.md │ ├── android │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── scala │ │ │ └── MainActivity.scala │ ├── core │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ ├── App.scala │ │ │ └── MainScreen.scala │ ├── desktop-awt │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ ├── desktop-native │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ └── html5 │ │ ├── index.html │ │ └── src │ │ └── main │ │ └── scala │ │ └── Main.scala ├── platformer │ ├── assets │ │ ├── drawable-mdpi │ │ │ ├── player.png │ │ │ ├── rat-trap-feature.png │ │ │ └── tileset.png │ │ └── levels │ │ │ └── level.json │ ├── core │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ ├── App.scala │ │ │ └── MainScreen.scala │ ├── desktop-awt │ │ └── src │ │ │ └── main │ │ │ └── scala │ │ │ └── Main.scala │ └── desktop-native │ │ └── src │ │ └── main │ │ └── scala │ │ └── Main.scala ├── reactive-bird │ └── README.md └── snake │ ├── README.md │ ├── core │ └── src │ │ └── main │ │ └── scala │ │ ├── App.scala │ │ └── MainScreen.scala │ ├── desktop-awt │ └── src │ │ └── main │ │ └── scala │ │ └── Main.scala │ ├── desktop-native │ └── src │ │ └── main │ │ └── scala │ │ └── Main.scala │ └── html5 │ ├── index.html │ └── src │ └── main │ └── scala │ └── Main.scala ├── html5 ├── cordova │ └── src │ │ └── main │ │ └── scala │ │ └── sgl │ │ └── html5 │ │ ├── CordovaApp.scala │ │ ├── CordovaMediaAudioProvider.scala │ │ ├── CordovaNativeAudioProvider.scala │ │ └── analytics │ │ └── CordovaFirebaseAnalyticsProvider.scala ├── firebase │ └── src │ │ └── main │ │ └── scala │ │ └── sgl │ │ └── html5 │ │ ├── analytics │ │ └── Html5FirebaseAnalyticsProvider.scala │ │ └── firebase │ │ └── Firebase.scala └── src │ ├── main │ └── scala │ │ └── sgl │ │ └── html5 │ │ ├── Html5App.scala │ │ ├── Html5AudioProvider.scala │ │ ├── Html5GraphicsProvider.scala │ │ ├── Html5InputProvider.scala │ │ ├── Html5SystemProvider.scala │ │ ├── Html5WindowProvider.scala │ │ ├── LocalStorageSave.scala │ │ ├── themes │ │ ├── DefaultTheme.scala │ │ ├── FixedWindowTheme.scala │ │ ├── FullScreenTheme.scala │ │ └── Theme.scala │ │ └── util │ │ ├── Html5ConsoleLoggingProvider.scala │ │ └── Html5JsonProvider.scala │ └── test │ └── scala │ └── sgl │ └── html5 │ └── util │ └── Html5JsonProvider.scala ├── jvm-shared └── src │ ├── main │ └── scala │ │ └── sgl │ │ └── util │ │ ├── FutureLoader.scala │ │ └── ThreadPoolSchedulerProvider.scala │ └── test │ └── scala │ └── sgl │ └── util │ └── FutureLoaderSuite.scala └── project ├── build.properties └── plugins.sbt /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 1.8 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 1.8 20 | - name: Set up Scala Native dependencies 21 | run: sudo apt-get install clang libunwind-dev libgc-dev libre2-dev libglu1-mesa-dev 22 | - name: Run tests 23 | run: sbt verifyCI 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .history 3 | *.swp 4 | *.swo 5 | *.log 6 | .metals/ 7 | .bloop/ 8 | project/metals.sbt 9 | *.tiled-project 10 | *.tiled-session 11 | .DS_Store 12 | .bsp 13 | -------------------------------------------------------------------------------- /BUGS.md: -------------------------------------------------------------------------------- 1 | # Known Issues 2 | 3 | ## Destkop AWT decoding of images 4 | 5 | imageio.read seems to misbehave on a 1-bit grayscale with alpha format: a fully 6 | transparent image encoded with 1 bit per value, where all the bits are 7 | transparent. The BufferedImage parsed by imageio is displayed fully black and 8 | does seem to be missing transparency. 9 | 10 | ### How to confirm 11 | 12 | output of `file` on the file: 13 | 14 | PNG image data, 150 x 200, 1-bit grayscale, non-interlaced 15 | 16 | output of `identify -verbose` on the file: 17 | Format: PNG (Portable Network Graphics) 18 | Mime type: image/png 19 | Class: DirectClass 20 | Geometry: 150x200+0+0 21 | Resolution: 1x1 22 | Print size: 150x200 23 | Units: Undefined 24 | Type: Bilevel 25 | Base type: Bilevel 26 | Endianess: Undefined 27 | Colorspace: Gray 28 | Depth: 8/1-bit 29 | Channel depth: 30 | gray: 1-bit 31 | alpha: 1-bit 32 | Channel statistics: 33 | Pixels: 30000 34 | Gray: 35 | min: 0 (0) 36 | max: 0 (0) 37 | mean: 0 (0) 38 | standard deviation: 0 (0) 39 | kurtosis: 0 40 | skewness: 0 41 | entropy: -nan 42 | Alpha: 43 | min: 0 (0) 44 | max: 0 (0) 45 | mean: 0 (0) 46 | standard deviation: 0 (0) 47 | kurtosis: 0 48 | skewness: 0 49 | entropy: -nan 50 | Alpha: graya(0,0) #00000000 51 | Colors: 1 52 | Histogram: 53 | 30000: ( 0, 0, 0, 0) #00000000 graya(0,0) 54 | Rendering intent: Undefined 55 | Gamma: 0.45455 56 | Chromaticity: 57 | red primary: (0.64,0.33) 58 | green primary: (0.3,0.6) 59 | blue primary: (0.15,0.06) 60 | white point: (0.3127,0.329) 61 | Background color: graya(255,1) 62 | Border color: graya(223,1) 63 | Matte color: graya(189,1) 64 | Transparent color: graya(0,0) 65 | Interlace: None 66 | Intensity: Undefined 67 | Compose: Over 68 | Page geometry: 150x200+0+0 69 | Dispose: Undefined 70 | Iterations: 0 71 | Compression: Zip 72 | Orientation: Undefined 73 | Properties: 74 | Creator: Adobe After Effects 75 | date:create: 2018-02-14T21:28:25+01:00 76 | date:modify: 2018-02-14T21:28:25+01:00 77 | png:bKGD: chunk was found (see Background color, above) 78 | png:cHRM: chunk was found (see Chromaticity, above) 79 | png:gAMA: gamma=0.45455 (See Gamma, above) 80 | png:IHDR.bit-depth-orig: 1 81 | png:IHDR.bit_depth: 1 82 | png:IHDR.color-type-orig: 0 83 | png:IHDR.color_type: 0 (Grayscale) 84 | png:IHDR.interlace_method: 0 (Not interlaced) 85 | png:IHDR.width,height: 150, 200 86 | png:pHYs: x_res=1, y_res=1, units=0 87 | png:text: 3 tEXt/zTXt/iTXt chunks were found 88 | png:tIME: 2018-02-09T14:30:39Z 89 | png:tRNS: chunk was found 90 | signature: 9a413b131ecf0ccfb02a837ddb33766d8604cc00da7937b79bd363b53c8d7d86 91 | Artifacts: 92 | filename: out/snow_00000.png 93 | verbose: true 94 | Tainted: False 95 | Filesize: 349B 96 | Number pixels: 30K 97 | Pixels per second: 0B 98 | User time: 0.000u 99 | Elapsed time: 0:01.000 100 | Version: ImageMagick 6.9.0-3 Q16 x86_64 2015-02-15 http://www.imagemagick.org 101 | 102 | ### Solution 103 | 104 | We probably need a more robust image decoding library for the AWT backend. The 105 | workaround for now is to re-encode the image in a more classic RGB with alpha, 106 | instead of using the more compact 1 bit encoding. 107 | 108 | ## Play Sound hangs forever on AWT 109 | 110 | It happens on the call to .close() on the AWT Clip object. This seems due to the 111 | default OpenJDK implementation of sampled sound with PulseAudio (/etc/java-8-openjdk/sound.properties): 112 | 113 | javax.sound.sampled.Clip=org.classpath.icedtea.pulseaudio.PulseAudioMixerProvider 114 | javax.sound.sampled.Port=org.classpath.icedtea.pulseaudio.PulseAudioMixerProvider 115 | javax.sound.sampled.SourceDataLine=org.classpath.icedtea.pulseaudio.PulseAudioMixerProvider 116 | javax.sound.sampled.TargetDataLine=org.classpath.icedtea.pulseaudio.PulseAudioMixerProvider 117 | 118 | Replace the above in the same configuration file by: 119 | 120 | javax.sound.sampled.Clip=com.sun.media.sound.DirectAudioDeviceProvider 121 | javax.sound.sampled.Port=com.sun.media.sound.PortMixerProvider 122 | javax.sound.sampled.SourceDataLine=com.sun.media.sound.DirectAudioDeviceProvider 123 | javax.sound.sampled.TargetDataLine=com.sun.media.sound.DirectAudioDeviceProvider 124 | 125 | This should fix the problem. 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Regis Blanc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /android/README.md: -------------------------------------------------------------------------------- 1 | # SGL Android 2 | 3 | Building SGL for Android is getting quite challenging due to the divergence between Android and Scala. The last 4 | version of Scala that can still target Android is 2.11.12 (pretty old yeah). In addition, the `sbt-android` plugin 5 | is not compatible with SBT `1.0`, so we need to use the latest `0.13.18` SBT version. 6 | 7 | To make things worse, these versions of SBT and Scala interacts weirdly with Java 9+, so in order to run `sbt` properly 8 | and build the library and a game APK, you need to run the Java 8 SDK (yes, from a different epoque). Also it seems like 9 | the latest Android tools for Linux requires at least Java 11 to use (for things like `sdkmanager` and other command-line tools), so you will 10 | have fun switching between multiple java version. 11 | 12 | Anyway, good luck. 13 | 14 | ## In Summary 15 | 16 | * Run regular `sbt` for the core projects, but build artifacts to target Scala 17 | `2.11.12`. 18 | * Run `sbt` version `0.13.18` with Java 8 for the `android/` project. Build 19 | artifacts to target Scala `2.11.12`. 20 | * Run Android CLI tools with `Java 11` (not all need it, but at least `sdkmanager`). 21 | * Android SDK must be downloaded and installed in home directory, then set 22 | `ANDROID_HOME` to the root. The `sbt-android` plugin should use this and 23 | automatically download what it needs. 24 | 25 | ## How to use 26 | 27 | TODO, extract important parts of https://github.com/scala-android/sbt-android for how to package/sign/release 28 | -------------------------------------------------------------------------------- /android/admob/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/AndroidOpenGLGraphicsProvider.scala: -------------------------------------------------------------------------------- 1 | 2 | //class GameView(attributeSet: AttributeSet) extends TextureView(AndroidApp.this) 3 | // with TextureView.SurfaceTextureListener { 4 | // 5 | // private implicit val LogTag = Logger.Tag("sgl-gameview") 6 | // 7 | // this.setSurfaceTextureListener(this) 8 | // this.setFocusable(true) 9 | // this.setContentDescription("Main View where the game is rendered.") 10 | // 11 | // override def onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int): Unit = { 12 | // logger.debug("onSurfaceTextureAvaialble called") 13 | // surfaceReady = true 14 | // } 15 | // 16 | // override def onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean = { 17 | // logger.debug("onSurfaceTextureDestroyed called") 18 | // surfaceReady = false 19 | 20 | // // We must ensure that the thread that is accessing this surface 21 | // // does no longer run at the end of the surfaceDestroyed callback, 22 | // // so we join on the thread. 23 | 24 | // // this is safe to call twice (also called in onPause) 25 | // gameLoop.running = false 26 | // // The thread should finish the current frame and then return the run method, 27 | // // so we just join on the thread to make sure there are no more rendering calls 28 | // // after returning this callback. 29 | // gameLoopThread.join 30 | 31 | // true 32 | // } 33 | 34 | // override def onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int): Unit = { 35 | // logger.debug("onSurfaceTextureSizeChanged called") 36 | // } 37 | // override def onSurfaceTextureUpdated(surface: SurfaceTexture): Unit = { 38 | // logger.debug("onSurfaceTextureUpdated called") 39 | // } 40 | // 41 | //} 42 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/AndroidSave.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package android 3 | 4 | import _root_.android.content.Context 5 | 6 | class AndroidSave(prefName: String, context: Context) extends AbstractSave { 7 | 8 | private val PreferenceFilename = prefName 9 | 10 | override def putInt(name: String, value: Int): Unit = { 11 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 12 | val editor = pref.edit 13 | editor.putInt(name, value) 14 | editor.commit() 15 | } 16 | 17 | override def getInt(name: String): Option[Int] = { 18 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 19 | if(pref.contains(name)) 20 | Some(pref.getInt(name, 0)) 21 | else 22 | None 23 | } 24 | 25 | //override to make it slighlty more efficient than using default implementation 26 | override def getIntOrElse(name: String, default: Int): Int = { 27 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 28 | pref.getInt(name, default) 29 | } 30 | 31 | override def incInt(name: String, increment: Int = 1): Int = { 32 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 33 | val current = pref.getInt(name, 0) 34 | val newVal = current + increment 35 | val editor = pref.edit 36 | editor.putInt(name, newVal) 37 | editor.commit() 38 | newVal 39 | } 40 | 41 | override def putLong(name: String, value: Long): Unit = { 42 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 43 | val editor = pref.edit 44 | editor.putLong(name, value) 45 | editor.commit() 46 | } 47 | override def getLong(name: String): Option[Long] = { 48 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 49 | if(pref.contains(name)) 50 | Some(pref.getLong(name, 0)) 51 | else 52 | None 53 | } 54 | override def getLongOrElse(name: String, default: Long): Long = { 55 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 56 | pref.getLong(name, default) 57 | } 58 | 59 | 60 | override def putBoolean(name: String, value: Boolean): Unit = { 61 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 62 | val editor = pref.edit 63 | editor.putBoolean(name, value) 64 | editor.commit() 65 | } 66 | override def getBoolean(name: String): Option[Boolean] = { 67 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 68 | if(pref.contains(name)) 69 | Some(pref.getBoolean(name, false)) 70 | else 71 | None 72 | } 73 | override def getBooleanOrElse(name: String, default: Boolean): Boolean = { 74 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 75 | pref.getBoolean(name, default) 76 | } 77 | 78 | 79 | override def putString(name: String, value: String): Unit = { 80 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 81 | val editor = pref.edit 82 | editor.putString(name, value) 83 | editor.commit() 84 | } 85 | override def getString(name: String): Option[String] = { 86 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 87 | if(pref.contains(name)) 88 | Some(pref.getString(name, "")) 89 | else 90 | None 91 | } 92 | override def getStringOrElse(name: String, default: String): String = { 93 | val pref = context.getSharedPreferences(PreferenceFilename, Context.MODE_PRIVATE) 94 | pref.getString(name, default) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/AndroidSystemProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package android 3 | 4 | import sgl.util._ 5 | 6 | import _root_.android.app.Activity 7 | import _root_.android.content.Intent 8 | import _root_.android.net.Uri 9 | import _root_.android.content.ActivityNotFoundException 10 | import java.net.URI 11 | 12 | import scala.concurrent.ExecutionContext 13 | 14 | trait AndroidSystemProvider extends SystemProvider with PartsResourcePathProvider { 15 | self: AndroidWindowProvider with Activity => 16 | 17 | object AndroidSystem extends System { 18 | 19 | override def exit(): Unit = { 20 | self.finish() 21 | } 22 | 23 | override def currentTimeMillis: Long = java.lang.System.currentTimeMillis 24 | override def nanoTime: Long = java.lang.System.nanoTime 25 | 26 | override def loadText(path: ResourcePath): Loader[Array[String]] = FutureLoader { 27 | try { 28 | val am = self.getAssets() 29 | val is = am.open(path.path) 30 | scala.io.Source.fromInputStream(is).getLines.toArray 31 | } catch { 32 | case (e: java.io.IOException) => 33 | throw new ResourceNotFoundException(path) 34 | } 35 | } 36 | 37 | override def loadBinary(path: ResourcePath): Loader[Array[Byte]] = FutureLoader { 38 | try { 39 | val am = self.getAssets() 40 | val is = am.open(path.path) 41 | val bis = new java.io.BufferedInputStream(is) 42 | val bytes = new scala.collection.mutable.ListBuffer[Byte] 43 | var b: Int = 0 44 | while({ b = bis.read; b != -1}) { 45 | bytes.append(b.toByte) 46 | } 47 | bytes.toArray 48 | } catch { 49 | case (e: java.io.IOException) => 50 | throw new ResourceNotFoundException(path) 51 | } 52 | } 53 | 54 | override def openWebpage(uri: URI): Unit = { 55 | val browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri.toString)) 56 | self.startActivity(browserIntent) 57 | } 58 | 59 | override def openGooglePlayApp(id: String, params: Map[String, String]): Unit = { 60 | try { 61 | val base = s"market://details?id=$id" 62 | val uri = Uri.parse(base + params.map{ case (k, v) => s"&$k=$v" }.mkString) 63 | val intent = new Intent(Intent.ACTION_VIEW, uri) 64 | self.startActivity(intent) 65 | } catch { 66 | case (ex: ActivityNotFoundException) => { 67 | // use the default implementation, which opens a webpage. 68 | super.openGooglePlayApp(id, params) 69 | } 70 | } 71 | } 72 | 73 | } 74 | val System = AndroidSystem 75 | 76 | override val ResourcesRoot = PartsResourcePath(Vector()) 77 | override val MultiDPIResourcesRoot = PartsResourcePath(Vector()) 78 | 79 | //Centralize the execution context used for asynchronous tasks in the Desktop backend 80 | //Could be overriden at wiring time 81 | implicit val executionContext: ExecutionContext = ExecutionContext.Implicits.global 82 | 83 | } 84 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/AndroidWindowProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package android 3 | 4 | import _root_.android.app.Activity 5 | import _root_.android.view.SurfaceView 6 | 7 | trait AndroidWindowProvider extends WindowProvider { 8 | this: AndroidApp => 9 | 10 | var gameView: GameView = null 11 | 12 | class AndroidWindow extends AbstractWindow { 13 | override def height = gameView.getHeight 14 | override def width = gameView.getWidth 15 | 16 | /* 17 | * Here's a bit of background based on the research I've done. 18 | * DisplayMetrics exports xdpi, ydpi, and densityDpi (and density). The 19 | * xdpi and ydpi are meant to be exact measurement of the screen physical 20 | * property, so they could be used when we need extremely precised pixel 21 | * manipulation. Obviously, they might be different, and technically the 22 | * pixels might not be squared. This is leading to a problem for how to set 23 | * ppi. 24 | * 25 | * The Android system is relying on DisplayMetrics.densityDpi for scaling 26 | * dp to px and for scaling bitmaps when they are not provided for the 27 | * right density (say you have a drawable-mdpi but you need a 28 | * drawable-hdpi). This densityDpi is not guaranteed to be the same as 29 | * xdpi/ydpi. In practice, it is often one of the standard bucket (160 30 | * (mdpi), 240 (hdpi), 320 (xhdpi), 480 (xxhdpi)). 31 | * 32 | * Interestingly, the densityDpi is not guaranteed to be one of the 33 | * standard bucket, and it could take a value in between. It's unlikely to 34 | * be a strange value like 193.14, but note that it seems like xdpi/ydpi 35 | * could take such strange values, because they are supposed to be the 36 | * exact measurements based on physical size of the pixels. For 37 | * densityDpi, device tends to choose to return a convenient value, either 38 | * one of the standard bucket, or a whole number in between. 39 | * 40 | * When Android tries to load a resource, it will use this densityDpi to 41 | * choose the best resource (mdpi/hdpi/xhdpi). If the densityDpi is between 42 | * two buckets, the system will then scale the bitmap to match the target 43 | * density, just like it would do if the resource was entirely missing (if 44 | * it needed a hdpi and only had a mdpi). 45 | * 46 | * The take-away there is that if a game wants to do any scaling of its own 47 | * coordinates (say for shapes drawned in the canvas), it should be done 48 | * using the densityDpi (and thus the Window.ppi) and never using the 49 | * xdpi/ydpi. Otherwise we run the risk that the resources will be scaled 50 | * differently than the scaling used for our custom canvas shapes. This is 51 | * a problem if the game assumes the standard sprite is say 32 pixel (in 52 | * mdpi), and starts drawing rect of 32 pixels (scaled at runtime to the 53 | * proper density), we must make sure the scaling is consistent with the 54 | * system scaling, so we must use densityDpi. 55 | */ 56 | 57 | override def xppi: Float = gameView.getResources.getDisplayMetrics.xdpi 58 | override def yppi: Float = gameView.getResources.getDisplayMetrics.ydpi 59 | 60 | // TODO: not exactly the ppi, we should be able to derive it from xppi and yppi? 61 | override def ppi: Float = gameView.getResources.getDisplayMetrics.densityDpi 62 | 63 | override def logicalPpi: Float = gameView.getResources.getDisplayMetrics.densityDpi 64 | } 65 | type Window = AndroidWindow 66 | override val Window = new AndroidWindow 67 | 68 | } 69 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/services/package.scala: -------------------------------------------------------------------------------- 1 | package sgl.android 2 | 3 | /** Provides Android implementation for games services 4 | * 5 | * Games services are features that request the use of a server to 6 | * sync data between players. The most common example is the Google 7 | * games services which provide out-of-the-box many useful services such 8 | * as leaderboards, achievements, and saved games. But leaderboard could 9 | * be provided by an alternative service as well, or maybe your own 10 | * custom implementation. 11 | * 12 | * Note that in theory achievements don't need a shared server, so maybe 13 | * this should not be part of services. 14 | */ 15 | package object services { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/util/AndroidJsonProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl.android.util 2 | 3 | import sgl.util.JsonProvider 4 | 5 | import org.json._ 6 | 7 | import scala.language.implicitConversions 8 | 9 | trait AndroidJsonProvider extends JsonProvider { 10 | 11 | object AndroidJson extends Json { 12 | 13 | type JValue = Any 14 | 15 | override def parse(raw: String): JValue = new JSONTokener(raw).nextValue() 16 | 17 | class AndroidRichJsonAst(v: Any) extends RichJsonAst { 18 | override def \ (field: String): JValue = v match { 19 | case (o: JSONObject) => { 20 | val r = o.opt(field) 21 | if(r == null) JNothing else r 22 | } 23 | case _ => JNothing 24 | } 25 | } 26 | override implicit def richJsonAst(ast: JValue): RichJsonAst = new AndroidRichJsonAst(ast) 27 | 28 | object AndroidJNothing 29 | type JNothing = AndroidJNothing.type 30 | override val JNothing: JNothing = AndroidJNothing 31 | 32 | type JNull = JSONObject.NULL.type 33 | override val JNull: JNull = JSONObject.NULL 34 | 35 | object AndroidJString extends JStringCompanion { 36 | override def unapply(ast: JValue): Option[String] = ast match { 37 | case (s: java.lang.String) => Some(s) 38 | case _ => None 39 | } 40 | } 41 | type JString = String 42 | override val JString: JStringCompanion = AndroidJString 43 | 44 | object AndroidJNumber extends JNumberCompanion { 45 | override def unapply(ast: JValue): Option[Double] = ast match { 46 | case (d: java.lang.Double) => Some(d) 47 | case (f: java.lang.Float) => Some(f.toDouble) 48 | case (i: java.lang.Integer) => Some(i.toDouble) 49 | case (l: java.lang.Long) => Some(l.toDouble) 50 | case _ => None 51 | } 52 | } 53 | type JNumber = Double 54 | override val JNumber: JNumberCompanion = AndroidJNumber 55 | 56 | object AndroidJBoolean extends JBooleanCompanion { 57 | override def unapply(ast: JValue): Option[Boolean] = ast match { 58 | case (b: java.lang.Boolean) => Some(b) 59 | case (b: Boolean) => Some(b) 60 | case _ => None 61 | } 62 | } 63 | type JBoolean = Boolean 64 | override val JBoolean: JBooleanCompanion = AndroidJBoolean 65 | 66 | object AndroidJObject extends JObjectCompanion { 67 | override def unapply(ast: JValue): Option[List[JField]] = ast match { 68 | case (o: JSONObject) => { 69 | val buffy = new scala.collection.mutable.ListBuffer[(String, Any)] 70 | val it = o.keys() 71 | while(it.hasNext) { 72 | val k = it.next() 73 | buffy.append((k, o.get(k))) 74 | } 75 | Some(buffy.toList) 76 | } 77 | case _ => None 78 | } 79 | } 80 | type JObject = JSONObject 81 | override val JObject: JObjectCompanion = AndroidJObject 82 | 83 | object AndroidJArray extends JArrayCompanion { 84 | override def unapply(ast: JValue): Option[List[JValue]] = ast match { 85 | case (a: JSONArray) => Some((0 until a.length()).map(i => a.get(i)).toList) 86 | case _ => None 87 | } 88 | } 89 | type JArray = JSONArray 90 | override val JArray: JArrayCompanion = AndroidJArray 91 | 92 | //type JField = (String, JValue) 93 | } 94 | override val Json: Json = AndroidJson 95 | 96 | } 97 | -------------------------------------------------------------------------------- /android/core/src/main/scala/sgl/android/util/AndroidLoggingProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl.android.util 2 | 3 | import sgl.util._ 4 | 5 | import android.util.Log 6 | 7 | /** Logging provider using android.util.Log 8 | * 9 | * Provide logging by delegating it to the standard android library 10 | * android.util.Log 11 | */ 12 | trait AndroidLoggingProvider extends LoggingProvider { 13 | 14 | import Logger._ 15 | 16 | abstract class LogLogger extends Logger { 17 | override protected def log(level: LogLevel, tag: Tag, msg: String): Unit = level match { 18 | case NoLogging => () 19 | case Error => Log.e(tag.name, msg) 20 | case Warning => Log.w(tag.name, msg) 21 | case Info => Log.i(tag.name, msg) 22 | case Debug => Log.d(tag.name, msg) 23 | case Trace => Log.v(tag.name, msg) 24 | } 25 | } 26 | } 27 | 28 | 29 | trait AndroidDefaultLoggingProvider extends AndroidLoggingProvider { 30 | case object DefaultLogLogger extends LogLogger { 31 | override val logLevel: Logger.LogLevel = Logger.Warning 32 | } 33 | override val logger = DefaultLogLogger 34 | } 35 | 36 | trait AndroidVerboseLoggingProvider extends AndroidLoggingProvider { 37 | case object VerboseLogLogger extends LogLogger { 38 | override val logLevel: Logger.LogLevel = Logger.Debug 39 | } 40 | override val logger = VerboseLogLogger 41 | } 42 | 43 | trait AndroidTracingLoggingProvider extends AndroidLoggingProvider { 44 | case object TracingLogLogger extends LogLogger { 45 | override val logLevel: Logger.LogLevel = Logger.Trace 46 | } 47 | override val logger = TracingLogLogger 48 | } 49 | -------------------------------------------------------------------------------- /android/firebase/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/google-analytics/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/google-play/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.18 2 | -------------------------------------------------------------------------------- /android/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.10") 2 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/Camera.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import geometry.Point 4 | 5 | class Camera { 6 | var x: Int = 0 7 | var y: Int = 0 8 | 9 | def coordinates: Point = Point(x, y) 10 | 11 | def cameraToWorld(p: Point): Point = Point(p.x + x, p.y + y) 12 | def worldToCamera(p: Point): Point = Point(p.x - x, p.y - y) 13 | 14 | override def toString: String = s"Camera(x=$x, y=$y)" 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/GameApp.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import util._ 4 | 5 | /** The most basic abstract game app 6 | * 7 | * A GameApp is a trait with the minimal requirement to build 8 | * a game. Any backend implementation should provide at least 9 | * a trait which is able to provide implementation for all of 10 | * thesse. 11 | * 12 | * However, one design philosophy of the SGL is to provide as 13 | * much flexibility as possible in how to compose the final configuration. 14 | * For example, if a game does not need audio, it is possible to just 15 | * manually wire the other dependencies, without the AudioProvider. 16 | * This GameApp trait is mostly a convenient and quick way to get 17 | * dependencies for a typical game. 18 | * 19 | * One must be careful to mix-in traits in the correct order because 20 | * of initializations. Many providers have abstract fields which default 21 | * to null if used in other trait initialization. Since the platform-specific 22 | * providers will override these, they must be inherited in a way that the 23 | * platform-specific providers are initialized before game-specific initialization 24 | * traits, so that the game-specific initialization code can make use of these 25 | * abstract values. Typically, if the game app defines their own AbstractApp that 26 | * represent the platform-independent game code: 27 | * trait AbstractApp extends GameApp with ... 28 | * then when instantiating for a specific platform, we should do: 29 | * object Html5Main extends Html5App with AbstractApp 30 | * This will ensure that all abstract values from providers are initialized by Html5 providers 31 | * and that they can be used pretty much anywhere (including in trait initialization) in the 32 | * game-specific code. 33 | */ 34 | trait GameApp extends GraphicsProvider with AudioProvider 35 | with WindowProvider with SystemProvider with LoggingProvider 36 | with GameLoopComponent with GameStateComponent with LifecycleListenerProvider 37 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/LifecycleListener.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | trait LifecycleListener { 4 | 5 | /* 6 | * Not sure how to best integrate this into the framework. 7 | * I started with using it in the different backend (as abstract override) to 8 | * add initialization code for the different provider (music provider, input provider, etc). 9 | * Since the exact ordering of initialization (which startup of which provider is invoked first) 10 | * depended on the order of mixins, it seems to not be the cleanest way to initialize the 11 | * framework. 12 | * 13 | * So the current design for intialization code of the framework (main class, starting the game loop, 14 | * listening to inputs, auto-pausing music when the underlying system pauses) is to do it 15 | * in the driver class (main activity in android, the main function is classical JVM app) and 16 | * initialize all the modules that need initialization from there, without relying on the Lifecycle. 17 | * 18 | * Lifecycle can still be used by client apps. 19 | */ 20 | 21 | /** Called at startup of the game application. 22 | * 23 | * Cake mixin has a somewhat fishy initialization order for 24 | * vals. This startup is called after the whole cake component 25 | * is initialized and can thus be used to perform some operations 26 | * that would be risky to perform in a trait body (constructor). 27 | */ 28 | def startup(): Unit 29 | 30 | /** Called when the app resumes after a pause. 31 | * 32 | * This is even invoked when the app first starts, after startup. 33 | * The reason to call it there, is that it is more natural to write 34 | * symmetric code between pause/resume, and you probably want the 35 | * first resume to happen when starting the game to not duplicate 36 | * starting code from startup. 37 | */ 38 | def resume(): Unit 39 | 40 | /** called when the application is paused. 41 | * 42 | * This happen when your game loses focus from the user, like if 43 | * another application is brought in front in Android, or it could 44 | * be minimized in Desktop. 45 | * 46 | * It is usually expected that the game will be resumed and a call 47 | * to resume will follow, but it is not guarentee and the game 48 | * might never come back. If possible, shutdown will be invoked before 49 | * quitting the app, but obviously this won't be the case if the app 50 | * is killed abruptly by the OS. 51 | */ 52 | def pause(): Unit 53 | 54 | /** Called just before the game application exists 55 | * 56 | * This might not be called, if the app is killed abruptly by the OS 57 | * (common on Android), but if the game shutdown normally, then this 58 | * will be invoked. 59 | */ 60 | def shutdown(): Unit 61 | 62 | 63 | //TODO: notify when the application window is being resized 64 | //def resize(width: Int, height: Int) 65 | } 66 | 67 | object SilentLifecyclieListener extends LifecycleListener { 68 | override def startup(): Unit = {} 69 | override def resume(): Unit = {} 70 | override def pause(): Unit = {} 71 | override def shutdown(): Unit = {} 72 | } 73 | 74 | trait LifecycleListenerProvider { 75 | 76 | val lifecycleListener: LifecycleListener = SilentLifecyclieListener 77 | 78 | } 79 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/achievements/Achievements.scala: -------------------------------------------------------------------------------- 1 | package sgl.achievements 2 | 3 | /* 4 | * An achievement system should be provided locally and backend-agnostic. 5 | * Additionnaly, we should provide achievements abstraction in services, 6 | * with concrete implemenation to different services (such as google game 7 | * services) 8 | */ 9 | trait AchievementsComponent { 10 | 11 | //this abstract type can become concrete in different backend implementation. 12 | //typically, android would set it to the string name 13 | type AchievementId 14 | 15 | case class Achievement(id: AchievementId, name: String) 16 | 17 | def unlockAchievement(achievement: Achievement): Unit 18 | } 19 | 20 | trait LocalSaveAchievements extends AchievementsComponent { 21 | 22 | type AchievementId = Int 23 | 24 | //TODO 25 | //override def unlockAchievement(achievement: Achievement): Unit 26 | 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/analytics/GameStateAutoAnalyticsComponent.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package analytics 3 | 4 | import sgl.util.LoggingProvider 5 | 6 | /** A GameStateComponent that automates analytics. 7 | * 8 | * This extends default game state implementation with 9 | * an implementation that automatically tracks game screen 10 | * navigation. 11 | * 12 | * If you want to use it, make sure to mix it in AFTER mixin the 13 | * standard App trait, since that trait provides the default 14 | * GameStateComponent, and you want this one to override it. 15 | * 16 | * We offer this as a separate component, so that client can 17 | * choose to not use analytics (no dependency to analytics in 18 | * the default GameStateComponent) or can choose more 19 | * fine grained way to track game screens, if necessary. 20 | */ 21 | trait GameStateAutoAnalyticsComponent extends GameStateComponent { 22 | this: GraphicsProvider with SystemProvider with LoggingProvider with AnalyticsProvider => 23 | 24 | override val gameState: GameState = new GameStateAutoAnalytics 25 | 26 | class GameStateAutoAnalytics extends GameState { 27 | override def pushScreen(screen: GameScreen): Unit = { 28 | Analytics.setGameScreen(screen) 29 | super.pushScreen(screen) 30 | } 31 | override def newScreen(screen: GameScreen): Unit = { 32 | Analytics.setGameScreen(screen) 33 | super.newScreen(screen) 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/analytics/LoggedAnalyticsProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package analytics 3 | 4 | import util.LoggingProvider 5 | 6 | /** An implementation of the Analytics module that only logs. 7 | * 8 | * This implementation relies on the logging module to log each event but it 9 | * does not send the data to an analysis service. One could technically 10 | * collect the logs to extract the data, but more likely this can be used when 11 | * one does not wish (or can't) to send analytics data. 12 | */ 13 | trait LoggedAnalyticsProvider extends AnalyticsProvider { 14 | this: GameStateComponent with LoggingProvider => 15 | 16 | class LoggedAnalytics extends Analytics { 17 | 18 | implicit val tag = Logger.Tag("analytics") 19 | 20 | override def logCustomEvent(name: String, params: EventParams): Unit = { 21 | logger.info(s"${name}: ${params}") 22 | } 23 | 24 | override def logLevelUpEvent(level: Long): Unit = { 25 | logger.info(s"level_up: {level=${level}}") 26 | } 27 | override def logLevelStartEvent(level: String): Unit = { 28 | logger.info(s"level_start: {level=${level}}") 29 | } 30 | override def logLevelEndEvent(level: String, success: Boolean): Unit = { 31 | logger.info(s"level_end: {level=${level}, success=${success}}") 32 | } 33 | override def logShareEvent(itemId: Option[String]): Unit = { 34 | logger.info(s"share: {item_id=${itemId}}") 35 | } 36 | override def logGameOverEvent(score: Option[Long], map: Option[String]): Unit = { 37 | logger.info(s"game_over: {score=${score}, map=${map}}") 38 | } 39 | override def logBeginTutorialEvent(): Unit = { 40 | logger.info(s"begin_tutorial") 41 | } 42 | override def logCompleteTutorialEvent(): Unit = { 43 | logger.info(s"complete_tutorial") 44 | } 45 | override def logUnlockAchievementEvent(achievement: String): Unit = { 46 | logger.info(s"unlock_achievement: {achievement=${achievement}}") 47 | } 48 | override def logPostScoreEvent(score: Long, level: Option[Long], character: Option[String]): Unit = { 49 | logger.info(s"post_score: {score=${score}, level=${level}, character=${character}}") 50 | } 51 | 52 | override def setGameScreen(gameScreen: GameScreen): Unit = { 53 | logger.info(s"setting current game screen: $gameScreen") 54 | } 55 | 56 | override def setPlayerProperty(name: String, value: String): Unit = { 57 | logger.info(s"setting player property ${name}=${value}") 58 | } 59 | } 60 | 61 | override val Analytics: Analytics = new LoggedAnalytics 62 | 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/analytics/NoAnalyticsProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package analytics 3 | 4 | /** An implementation of the Analytics module that does nothing. 5 | * 6 | * Use this if you want to totally ignore logging, for example 7 | * for a release version on a platform that has no good analytics 8 | * framework. Generally you should use LoggedAnalyticsProvider as 9 | * it helps with debugging, but this version is more lean with 10 | * less dependencies and it might make sense if you don't want 11 | * a release version to actually log the analytics. 12 | */ 13 | trait NoAnalyticsProvider extends AnalyticsProvider { 14 | this: GameStateComponent => 15 | 16 | class NoAnalytics extends Analytics { 17 | 18 | override def logCustomEvent(name: String, params: EventParams): Unit = {} 19 | 20 | override def logLevelUpEvent(level: Long): Unit = {} 21 | override def logLevelStartEvent(level: String): Unit = {} 22 | override def logLevelEndEvent(level: String, success: Boolean): Unit = {} 23 | override def logShareEvent(itemId: Option[String]): Unit = {} 24 | override def logGameOverEvent(score: Option[Long], map: Option[String]): Unit = {} 25 | override def logBeginTutorialEvent(): Unit = {} 26 | override def logCompleteTutorialEvent(): Unit = {} 27 | override def logUnlockAchievementEvent(achievement: String): Unit = {} 28 | override def logPostScoreEvent(score: Long, level: Option[Long], character: Option[String]): Unit = {} 29 | 30 | override def setGameScreen(gameScreen: GameScreen): Unit = {} 31 | 32 | override def setPlayerProperty(name: String, value: String): Unit = {} 33 | } 34 | 35 | override val Analytics: Analytics = new NoAnalytics 36 | 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Circle.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | case class Circle(x: Float, y: Float, radius: Float) { 4 | require(radius >= 0) 5 | 6 | def center: Point = Point(x, y) 7 | 8 | def left: Float = x - radius 9 | def top: Float = y - radius 10 | def right: Float = x + radius 11 | def bottom: Float = y + radius 12 | 13 | def +(m: Vec): Circle = Circle(x + m.x, y + m.y, radius) 14 | def -(m: Vec): Circle = Circle(x - m.x, y - m.y, radius) 15 | 16 | def intersect(x: Float, y: Float): Boolean = { 17 | val d2 = (x - this.x)*(x - this.x) + (y - this.y)*(y - this.y) 18 | d2 <= radius*radius 19 | } 20 | 21 | def intersect(point: Point): Boolean = intersect(point.x, point.y) 22 | 23 | def intersect(that: Circle): Boolean = Collisions.circleWithCircle(this, that) 24 | 25 | def intersect(rect: Rect): Boolean = Collisions.circleWithAabb(this, rect) 26 | 27 | def boundingBox: Rect = Rect(x - radius, y - radius, 2*radius, 2*radius) 28 | 29 | def asEllipse: Ellipse = Ellipse(x, y, 2*radius, 2*radius) 30 | } 31 | 32 | object Circle { 33 | def apply(center: Point, radius: Float): Circle = Circle(center.x, center.y, radius) 34 | } 35 | 36 | 37 | case class Ellipse(x: Float, y: Float, width: Float, height: Float) { 38 | 39 | def center: Point = Point(x, y) 40 | 41 | def left: Float = x - width/2 42 | def top: Float = y - height/2 43 | def right: Float = x + width/2 44 | def bottom: Float = y + height/2 45 | 46 | def +(m: Vec): Ellipse = Ellipse(x + m.x, y + m.y, width, height) 47 | def -(m: Vec): Ellipse = Ellipse(x - m.x, y - m.y, width, height) 48 | 49 | def boundingBox: Rect = Rect(x - width/2, y - height/2, width, height) 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Collisions.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | object Collisions { 4 | 5 | def circleWithCircle(c1: Circle, c2: Circle): Boolean = { 6 | val dist = (c1.center - c2.center) 7 | val n2 = dist.x*dist.x + dist.y*dist.y 8 | n2 <= (c1.radius+c2.radius)*(c1.radius+c2.radius) 9 | } 10 | 11 | def aabbWithAabb(r1: Rect, r2: Rect): Boolean = { 12 | val noCollision = r2.left >= r1.left + r1.width || 13 | r2.left + r2.width <= r1.left || 14 | r2.top >= r1.top + r1.height || 15 | r2.top + r2.height <= r1.top 16 | !noCollision 17 | } 18 | 19 | def circleWithAabb(c: Circle, r: Rect): Boolean = { 20 | val circleBoundingBox = c.boundingBox 21 | if(!aabbWithAabb(circleBoundingBox, r)) { 22 | //no collision with overapproximation rect means for sure no collision 23 | false 24 | } else if(r.vertices.exists(p => c.intersect(p))) { 25 | //if one of the vertices of rect is in circle, we found a real collision 26 | true 27 | } else if(r.intersect(c.center)) { 28 | true 29 | } else { 30 | /* finally, there are two remaining cases. Either the circle intersects the 31 | * rectangle from one of the side, or it does not intersect at all. 32 | */ 33 | val verticalProj = projectsOnSegment(c.center, r.topLeft, r.bottomLeft) 34 | val horizontalProj = projectsOnSegment(c.center, r.topLeft, r.topRight) 35 | if(verticalProj || horizontalProj) 36 | true 37 | else 38 | false 39 | } 40 | } 41 | 42 | /** Check collision of convex polygon versus polygon using Separated Axis Theorem. 43 | * 44 | * The SAT technique compares the shadows of each polygon along all axis 45 | * defined by all lines of both polygons, and if they don't overlap in any 46 | * of them, then the polygons do not intersect. This only works with convex 47 | * polygons. 48 | * 49 | * Axis are created by taking the normal of each line of each polygons. 50 | */ 51 | def polygonWithPolygonSat(p1: Polygon, p2: Polygon): Boolean = { 52 | 53 | // Check for all axis of p1, that the shadows overlap. 54 | def check(p1: Polygon, p2: Polygon): Boolean = { 55 | for(i <- 0 until p1.nbEdges) { 56 | val a = p1.edgeStart(i) 57 | val b = p1.edgeEnd(i) 58 | val n = (b-a).normal 59 | 60 | // We consider the axis defined by the normal to the line segment a->b. 61 | // We project all points to it to get a range for the shadow. 62 | 63 | var p1min = Float.MaxValue 64 | var p1max = Float.MinValue 65 | for(v <- p1.vertices) { 66 | val dp = v*n 67 | p1min = p1min min dp 68 | p1max = p1max max dp 69 | } 70 | 71 | var p2min = Float.MaxValue 72 | var p2max = Float.MinValue 73 | for(v <- p2.vertices) { 74 | val dp = v*n 75 | p2min = p2min min dp 76 | p2max = p2max max dp 77 | } 78 | 79 | // Finally, check if they overlap. 80 | if(p1min > p2max || p2min > p1max) 81 | return false 82 | } 83 | return true 84 | } 85 | check(p1, p2) && check(p2, p1) 86 | } 87 | 88 | private def projectsOnSegment(c: Point, ss: Point, se: Point): Boolean = { 89 | val s1 = (c - ss) * (se - ss) 90 | val s2 = (c - se) * (se - ss) 91 | s1*s2 < 0 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Line.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | case class Line(xa: Float, ya: Float, xb: Float, yb: Float) { 4 | 5 | def a: Point = Point(xa, ya) 6 | def b: Point = Point(xb, yb) 7 | 8 | } 9 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Point.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | import scala.language.implicitConversions 4 | 5 | /* 6 | * Point and Vec are floats as they are used to simulate physics 7 | * and using Int (pixels) tend to lose precision when a frame only 8 | * move a fraction of a pixel. 9 | */ 10 | 11 | case class Point(var x: Float, var y: Float) { 12 | 13 | def +(v: Vec): Point = Point(x+v.x, y+v.y) 14 | def -(v: Vec): Point = Point(x-v.x, y-v.y) 15 | 16 | def translate(v: Vec): Unit = { 17 | this.x += v.x 18 | this.y += v.y 19 | } 20 | 21 | def unary_- : Point = Point(-x, -y) 22 | 23 | def -(p: Point): Vec = Vec(x - p.x, y - p.y) 24 | 25 | /* trying to find a nice way to create a vector from this to that */ 26 | def -->(that: Point): Vec = that - this 27 | 28 | def withX(nx: Float): Point = Point(nx, this.y) 29 | def withY(ny: Float): Point = Point(this.x, ny) 30 | 31 | def toVec: Vec = Vec(x, y) 32 | } 33 | 34 | object Point { 35 | implicit def tupleToPoint(p: (Float, Float)): Point = new Point(p._1, p._2) 36 | implicit def intTupleToPoint(p: (Int, Int)): Point = new Point(p._1, p._2) 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Polygon.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | class Polygon(val vertices: Vector[Vec]) { 4 | require(vertices.size > 2) 5 | 6 | def nbEdges: Int = vertices.size 7 | 8 | def edgeStart(i: Int): Vec = vertices(i) 9 | def edgeEnd(i: Int): Vec = vertices((i+1) % vertices.size) 10 | 11 | def boundingBox: Rect = { 12 | var top: Float = Float.MaxValue 13 | var bottom: Float = Float.MinValue 14 | var left: Float = Float.MaxValue 15 | var right: Float = Float.MinValue 16 | 17 | for(v <- vertices) { 18 | top = top min v.y 19 | bottom = bottom max v.y 20 | left = left min v.x 21 | right = right max v.x 22 | } 23 | 24 | Rect.fromBoundingBox(left=left, top=top, right=right, bottom=bottom) 25 | } 26 | 27 | // TODO: Currently we assume that we have convex polygons, as our only 28 | // collision detection method for polygons need convex polygons. We should 29 | // add code to detect convex/concave polygons, and then some way to 30 | // do triangulation in order to transform a concave polygon into a set of 31 | // convex polygons that we can then apply our collision algorithm on. 32 | // - https://en.wikipedia.org/wiki/Polygon_triangulation 33 | // - https://math.stackexchange.com/questions/1743995/determine-whether-a-polygon-is-convex-based-on-its-vertices 34 | // - https://stackoverflow.com/questions/471962/how-do-i-efficiently-determine-if-a-polygon-is-convex-non-convex-or-complex 35 | } 36 | 37 | object Polygon { 38 | def apply(vertices: Vector[Vec]) = new Polygon(vertices) 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Rect.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | /** an AABB Rect. 4 | * 5 | * This class is mutable, and several of its methods modify the state instead 6 | * of just returning a new Rect. This is very much not idiomatic Scala (which 7 | * favors immutable objects), but this is also a trade-off necessary for games 8 | * to avoid generating too much garbage to collect. Time will tell if this 9 | * design decision was good or bad. 10 | **/ 11 | class Rect(var left: Float, var top: Float, var width: Float, var height: Float) { 12 | 13 | def right: Float = left + width 14 | def bottom: Float = top + height 15 | 16 | def centerX = left + width/2 17 | def centerY = top + height/2 18 | def center: Point = Point(centerX, centerY) 19 | 20 | def +(m: Vec): Rect = Rect(left + m.x, top + m.y, width, height) 21 | def -(m: Vec): Rect = Rect(left - m.x, top - m.y, width, height) 22 | 23 | /* 24 | * names are inversed with (x,y) coordinates, unfortunate... 25 | */ 26 | def topLeft: Point = Point(left, top) 27 | def topRight: Point = Point(right, top) 28 | def bottomLeft: Point = Point(left, bottom) 29 | def bottomRight: Point = Point(right, bottom) 30 | 31 | def vertices: Set[Point] = Set(topLeft, topRight, bottomLeft, bottomRight) 32 | 33 | //maybe intersect should go into external objects since there is no notion of direction (point vs rect) 34 | def intersect(x: Float, y: Float): Boolean = 35 | x >= left && x <= right && y >= top && y <= bottom 36 | 37 | def intersect(point: Point): Boolean = intersect(point.x, point.y) 38 | 39 | def intersect(rect: Rect): Boolean = Collisions.aabbWithAabb(this, rect) 40 | 41 | def intersect(circle: Circle): Boolean = Collisions.circleWithAabb(circle, this) 42 | 43 | override def toString: String = s"Rect(left=$left, top=$top, width=$width, height=$height)" 44 | 45 | override def clone: Rect = Rect(left, top, width, height) 46 | } 47 | 48 | object Rect { 49 | 50 | def apply(left: Float, top: Float, width: Float, height: Float) = new Rect(left, top, width, height) 51 | 52 | def fromBoundingBox(left: Float, top: Float, right: Float, bottom: Float): Rect = 53 | Rect(left, top, right - left, bottom - top) 54 | 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/Vec.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | //import scala.language.implicitConversions 4 | 5 | /* 6 | * We use different class for Point and Vec, even though 7 | * they can be seen as the same object, because it seems 8 | * like we don't want to accidently replace one by another. 9 | */ 10 | case class Vec(x: Float, y: Float) { 11 | 12 | def +(m: Vec): Vec = Vec(x+m.x, y+m.y) 13 | def -(m: Vec): Vec = Vec(x-m.x, y-m.y) 14 | 15 | def *(s: Float): Vec = Vec(x*s, y*s) 16 | 17 | def unary_- : Vec = Vec(-x, -y) 18 | 19 | def norm: Float = math.sqrt(x*x + y*y).toFloat 20 | 21 | def normal: Vec = Vec(-y, x) 22 | 23 | def normalized: Vec = { 24 | val n = this.norm 25 | Vec(x/n, y/n) 26 | } 27 | 28 | def isZero: Boolean = x == 0 && y == 0 29 | def nonZero: Boolean = !isZero 30 | 31 | def pmax(that: Vec): Vec = Vec(x max that.x, y max that.y) 32 | def pmin(that: Vec): Vec = Vec(x min that.x, y min that.y) 33 | 34 | def dotProduct(that: Vec): Float = this.x*that.x + this.y*that.y 35 | def *(that: Vec): Float = this.dotProduct(that) 36 | 37 | //TODO: could define +=, *=, -= as mutating the Vec? would make much 38 | // code similar, we would still keep +/*/- as operation that 39 | // produces new Vec, but there would be the option of not creating garbage 40 | 41 | //clockwise as the standard mathematical interpetation (with y pointing up), 42 | //so might be reversed in the game engine world 43 | def clockwisePerpendicular: Vec = Vec(-y, x) 44 | 45 | def toPoint: Point = Point(x, y) 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/geometry/package.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | /** Definitions to handle geometry and collisions. 4 | * 5 | * All coordinates should be in Double and/or Float, This is both to support 6 | * world coordinates (expressed at Float-precision) and and because Int are a 7 | * bit tricky and often need to be converted in floating point in middle of an 8 | * operation. 9 | * 10 | * For example, a circle could be defined with Int pixel center, and Int 11 | * radius, but there will be some pixels that are only partially in the 12 | * area of the circle. Better to work with Double all the way. 13 | * 14 | * Double are also needed for Point and Vec, as part of the simulation of 15 | * physics. That is because some objects will not move 1 whole pixel during 16 | * intermediate frames, but we still need to accumulate the progress somehow, 17 | * hence the need for floating points. 18 | * 19 | * There is the question whether the GraphicsProvider should expose some interface 20 | * to draw geometry primitve like Rect or Circle, instead of taking individual coordinates. 21 | * It seems better to not mix geometry and GraphicsProvider, in order to not 22 | * impose the engine geometry system to users that only wishes to use the Graphics 23 | * for platform independence. 24 | * 25 | * Also, the axis system for the geometry follows the same axis system as the 26 | * rest of SGL, that is x is pointing right and y is pointing down (from 27 | * top-left to bottom-right). This might lead to some confusion because 28 | * standard math definition is to have y pointing up, and most algorithms here 29 | * are purely mathematical on geometric shapes. There's still more value in having 30 | * a consistent axis system in all of SGL though. 31 | */ 32 | package object geometry { 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/package.scala: -------------------------------------------------------------------------------- 1 | /** Scala Game Library 2 | * 3 | * This is the entry point to the SGL, a Scala Game Library to develop 4 | * cross-platform games in a completely type safe manner. 5 | * 6 | * The whole game engine is build around the concept of a giant cake 7 | * pattern. This root package provides a bunch of trait, with two 8 | * main families ending in *Component and in *Provider. The Provider 9 | * traits are abstractions of platform specific concepts, here is 10 | * a list: 11 | * 12 | * - {{sgl.GraphicsProvider}} 13 | * - {{sgl.AudioProvider}} 14 | * - {{sgl.SystemProvider}} 15 | * - {{sgl.WindowProvider}} 16 | * 17 | * They are implemented in the various backend provided on separate projects. 18 | * Component trait are usually cross-platform implementation, but they are 19 | * still traits as they usually depend on the providers. 20 | * 21 | * The design principle behind the library is that a game is represented as one 22 | * instance of the cake, and thus many things have a single access point. The 23 | * WindowProvider exposes data such as the window dimensions, and the dpi of the 24 | * screen. It essentially assumes that one global window object is available to the 25 | * program. This means a user of the library has no explicit control over the window/screen 26 | * and is simply provided by a container that exposes the basic infrastructure of the 27 | * game instance. 28 | * 29 | */ 30 | package object sgl { 31 | 32 | 33 | 34 | } 35 | 36 | 37 | /* TODO 38 | * 39 | * I'd like to do a large refactoring and expose a simpler lifecycle for end implementations. 40 | * 41 | * Instead of mixing LifecycleProvider and GameLoopState, I think we need to define the set 42 | * of functions that will be called (init, shutdown, resume, pause, update) by the framework, 43 | * and any game simply implements the ones that they need. This unifies lifecycleprovider into 44 | * the core game loop, and also remove all the complexity of the GameScreen abstractions, which 45 | * should be moved to something built on top of the base abstraction, just like a scene 2D would 46 | * be. The game state and GameScreen needs to become obsolete and just one possible abstraction on 47 | * top of the core lifecycle methods. 48 | */ 49 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/proxy/ProxiedGameApp.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package proxy 3 | 4 | trait ProxiedGameApp extends GameApp { 5 | 6 | // TODO: implement these life cycle methods. 7 | //def startup(): Unit = {} 8 | //def resize(width: Int, height: Int): Unit 9 | //def resume(): Unit = {} 10 | //def pause(): Unit = {} 11 | //def shutdown(): Unit = {} 12 | 13 | def update(dt: Long, canvas: CanvasProxy): Unit 14 | 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/proxy/ProxyPlatformProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package proxy 3 | 4 | import sgl.util.SchedulerProvider 5 | import sgl.util.NoLoggingProvider 6 | 7 | trait ProxyPlatformProvider extends ProxiedGameApp with SchedulerProvider 8 | with ProxySystemProvider with ProxyWindowProvider with ProxySchedulerProvider 9 | with ProxyGraphicsProvider 10 | with FakeAudioProvider with NoLoggingProvider { 11 | 12 | val PlatformProxy: PlatformProxy 13 | 14 | override def update(dt: Long, canvas: CanvasProxy): Unit = this.gameLoopStep(dt, Graphics.ProxyCanvas(canvas)) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/proxy/ProxySchedulerProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package proxy 3 | 4 | import sgl.util._ 5 | 6 | trait ProxySchedulerProvider extends SchedulerProvider { 7 | 8 | val PlatformProxy: PlatformProxy 9 | 10 | class ProxyScheduler extends Scheduler { 11 | override def schedule(task: ChunkedTask): Unit = PlatformProxy.schedulerProxy.schedule(task) 12 | } 13 | override val Scheduler: Scheduler = new ProxyScheduler 14 | 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/proxy/ProxySystemProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package proxy 3 | 4 | import sgl.util._ 5 | 6 | trait ProxySystemProvider extends SystemProvider { 7 | 8 | val PlatformProxy: PlatformProxy 9 | 10 | object ProxySystem extends System { 11 | def exit(): Unit = PlatformProxy.systemProxy.exit() 12 | def currentTimeMillis: Long = PlatformProxy.systemProxy.currentTimeMillis 13 | def nanoTime: Long = PlatformProxy.systemProxy.nanoTime 14 | def loadText(path: ResourcePath): Loader[Array[String]] = PlatformProxy.systemProxy.loadText(path.path) 15 | def loadBinary(path: ResourcePath): Loader[Array[Byte]] = PlatformProxy.systemProxy.loadBinary(path.path) 16 | def openWebpage(uri: java.net.URI): Unit = PlatformProxy.systemProxy.openWebpage(uri) 17 | } 18 | override val System: System = ProxySystem 19 | 20 | case class ProxyResourcePath(path: ResourcePathProxy) extends AbstractResourcePath { 21 | override def / (filename: String): ResourcePath = ProxyResourcePath(path / filename) 22 | def extension: Option[String] = path.extension 23 | } 24 | type ResourcePath = ProxyResourcePath 25 | 26 | override def ResourcesRoot: ResourcePath = ProxyResourcePath(PlatformProxy.resourcesRoot) 27 | override def MultiDPIResourcesRoot: ResourcePath = ProxyResourcePath(PlatformProxy.multiDPIResourcesRoot) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/proxy/ProxyWindowProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package proxy 3 | 4 | trait ProxyWindowProvider extends WindowProvider { 5 | 6 | val PlatformProxy: PlatformProxy 7 | 8 | class ProxyWindow extends AbstractWindow { 9 | override def width: Int = PlatformProxy.windowProxy.width 10 | override def height: Int = PlatformProxy.windowProxy.height 11 | override def xppi: Float = PlatformProxy.windowProxy.xppi 12 | override def yppi: Float = PlatformProxy.windowProxy.yppi 13 | override def logicalPpi: Float = PlatformProxy.windowProxy.logicalPpi 14 | } 15 | type Window = ProxyWindow 16 | override val Window = new ProxyWindow 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/scene/Action.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package scene 3 | 4 | 5 | /** An action to be performed by a SceneElement 6 | * 7 | * Can be attached to SceneElement, provide some action 8 | * to be performed in the update methods. Are convenient 9 | * to organize code, give a way to extend a SceneElement 10 | * behaviour from outside, or with standard actions. 11 | * 12 | * An Action is stateful, keeping some internal state while performing 13 | * the update. It is automatically removed from the SceneElement 14 | * when the Action is completed (TODO: that means GC will eventually 15 | * be needed, might be ok, but dont really know). 16 | */ 17 | abstract class Action { 18 | def update(dt: Long): Unit 19 | def isCompleted: Boolean 20 | } 21 | 22 | class SequenceAction(private var as: List[Action]) extends Action { 23 | override def update(dt: Long): Unit = as match { 24 | case x::xs => 25 | x.update(dt) 26 | if(x.isCompleted) 27 | as = xs 28 | case Nil => 29 | () 30 | } 31 | override def isCompleted: Boolean = as.isEmpty 32 | } 33 | 34 | class ParallelAction(private var as: List[Action]) extends Action { 35 | override def update(dt: Long): Unit = { 36 | as.foreach(a => a.update(dt)) 37 | as.filterNot(_.isCompleted) 38 | } 39 | override def isCompleted: Boolean = as.isEmpty 40 | } 41 | 42 | /** Mix-in to any action to make it terminate after exact duration */ 43 | trait TemporalAction extends Action { 44 | 45 | val duration: Long 46 | 47 | private var age: Long = 0 48 | 49 | abstract override def update(dt: Long): Unit = { 50 | //TODO: should we cut to the maxAge ? if(age+dt > maxAge) maxAge-age 51 | super.update(dt) 52 | age += dt 53 | } 54 | 55 | override def isCompleted: Boolean = { 56 | age >= duration 57 | } 58 | 59 | } 60 | 61 | //TODO: define a MoveToAction, that takes a coordinates objective. The issue 62 | // is that this will need a pointer to a SceneElement, thus a dependency. 63 | // Similarly, the SceneElement is going to need a dependency to ActionComponent 64 | // which means that anyone that would like to use Scene without actions, will 65 | // still need to depend on the ActionComponent. Would be nice to have Action as 66 | // a completely independent mecanisms. Or worst case we expose in package.scala 67 | // a SceneComponent that combines the different Actions and Scene implementation scattered 68 | // accross the package 69 | // 70 | // It seems the dependency to SceneElement might be better captured with some trait 71 | // mixins, such as trait ElementWithAction 72 | 73 | 74 | //TODO: for avoiding GC, we could have an implicit pool of actions, with actions created through 75 | // a factory method. The factory would reuse actions with same type from the pool, reset them 76 | // and thus reuse them. 77 | // Could use implicit to have the pool passed around, mostly transparent, but we get total 78 | // control if we wish to not use the pool (if we know an Action is unique for example) 79 | //trait ActionPool[A <: Action] { 80 | // def make: A 81 | //} 82 | 83 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/scene/Scene.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package scene 3 | 4 | /** Component including all the scene features 5 | * 6 | * You can use this trait for easily depending on 7 | * all of the scene features, instead of importing 8 | * each individually. 9 | */ 10 | trait SceneComponent extends SceneGraphComponent with ui.ScrollPaneComponent { 11 | this: GraphicsProvider with WindowProvider with SystemProvider with ViewportComponent => 12 | 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/scene/package.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | package object scene { 4 | 5 | /** Provide a hierarchical scene of renderable objects 6 | * 7 | * This builds on top of the core GraphicsProvider to provide 8 | * a scene graph with a structured hierarchy to render 9 | * and organize objects on the screen. 10 | * 11 | * This handles translation from global to local coordinates, as well 12 | * as input routing (maybe only clicks?) to the correct children (and properly firing 13 | * inputs only if no overlapping elements has intercepted it). 14 | * 15 | * The main use case is to build game UIs, such as menus and HUD. 16 | * 17 | * In theory, you could build the whole game with the API, as it provides a 18 | * similar interface to the core GameScreen abstraction, while adding some 19 | * higher level concept such as a graph organization of the scene. 20 | * 21 | * It comes with built-in widgets in the sgl.scene.ui package, for common 22 | * UI elements such as buttons and textfield. 23 | */ 24 | 25 | // TODO: Offer a ScrollPane kind of container, which would provide 26 | // a default scrollable background (by touch and mouse scroll). 27 | // This would be great for building level menus. More details on 28 | // the design are within the Fish Escape source code (in the level menu). 29 | // This might in particular be a good point to expose a touch scroll API, 30 | // instead of within the core input handling system. Mostly because it's 31 | // not clear what is a scroll action versus a click/touch action. 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/scene/ui/Buttons.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package scene 3 | package ui 4 | 5 | trait ButtonsComponent { 6 | this: GraphicsProvider with WindowProvider with SceneComponent => 7 | 8 | import Graphics._ 9 | 10 | abstract class Button(_x: Float, _y: Float, _width: Float, _height: Float) 11 | extends SceneNode(_x, _y, _width, _height) { 12 | 13 | private var pressed: Boolean = false 14 | 15 | override def update(dt: Long): Unit = {} 16 | 17 | def renderRegular(canvas: Canvas): Unit 18 | def renderPressed(canvas: Canvas): Unit 19 | 20 | override def render(canvas: Canvas): Unit = { 21 | if(pressed) 22 | renderPressed(canvas) 23 | else 24 | renderRegular(canvas) 25 | } 26 | 27 | override def notifyDown(x: Float, y: Float): Unit = { 28 | pressed = true 29 | } 30 | override def notifyPointerLeave(): Unit = { 31 | pressed = false 32 | } 33 | override def notifyUp(x: Float, y: Float): Unit = { 34 | pressed = false 35 | } 36 | 37 | } 38 | 39 | class BitmapButton(_x: Float, _y: Float, regularBitmap: BitmapRegion, pressedBitmap: BitmapRegion) 40 | extends Button(_x, _y, regularBitmap.width, regularBitmap.height) { 41 | 42 | override def renderPressed(canvas: Canvas): Unit = 43 | canvas.drawBitmap(pressedBitmap, x, y) 44 | 45 | override def renderRegular(canvas: Canvas): Unit = 46 | canvas.drawBitmap(regularBitmap, x, y) 47 | } 48 | 49 | // Seems like we could use some general theme object, which provide the 50 | // look and feel of the interface. It would contain border color, border 51 | // width, corner (round vs square), fill color, margins, etc. But for now, 52 | // since we are just getting started, a simple button theme might be enough 53 | // for our needs. 54 | case class ButtonTheme( 55 | borderColor: Color, 56 | fillColor: Color, 57 | textColor: Color, 58 | textFont: Font 59 | ) { 60 | 61 | val borderPaint = defaultPaint.withColor(borderColor) 62 | val fillPaint = defaultPaint.withColor(fillColor) 63 | val textPaint = defaultPaint.withColor(textColor) 64 | 65 | def drawBox(canvas: Canvas, x: Float, y: Float, width: Float, height: Float): Unit = { 66 | canvas.drawRect(x, y, width, height, fillPaint) 67 | canvas.drawLine(x, y, x+width, y, borderPaint) 68 | canvas.drawLine(x+width, y, x+width, y+height, borderPaint) 69 | canvas.drawLine(x+width, y+height, x, y+height, borderPaint) 70 | canvas.drawLine(x, y+height, x, y, borderPaint) 71 | } 72 | 73 | } 74 | 75 | class TextButton(_x: Float, _y: Float, _width: Float, _height: Float, label: String, regularTheme: ButtonTheme, pressedTheme: ButtonTheme) 76 | extends Button(_x, _y, _width, _height) { 77 | 78 | // The verticalPadding is there to handle the location where we should draw 79 | // the label. As the drawString renders a string from the bottom left, and 80 | // the problem is that parts of the characters might be rendered below this 81 | // base line. We thus need to offset it from the actual height. Ideally we 82 | // should compute that from the rendering and font size, but this requires 83 | // a bunch of modification to the framework, so instead we hardcode a value 84 | // that is most likely fine, and the caller can override it if necessary. 85 | // We compute this by first removing half of the real padding (the height - 86 | // the font size) and then we add a small offset for the part of the font 87 | // that's below the baseline (using 20% as an all-around appromiation). 88 | val verticalPadding = (_height-regularTheme.textFont.size)/2 + (0.2f*regularTheme.textFont.size).toInt 89 | 90 | val regularTextPaint = defaultPaint.withColor(regularTheme.textColor).withFont(regularTheme.textFont).withAlignment(Alignments.Center) 91 | val pressedTextPaint = defaultPaint.withColor(pressedTheme.textColor).withFont(pressedTheme.textFont).withAlignment(Alignments.Center) 92 | 93 | override def renderPressed(canvas: Canvas): Unit = { 94 | pressedTheme.drawBox(canvas, x, y, _width, _height) 95 | canvas.drawString(label, x + width/2, y + height - verticalPadding, pressedTextPaint) 96 | } 97 | 98 | override def renderRegular(canvas: Canvas): Unit = { 99 | regularTheme.drawBox(canvas, x, y, _width, _height) 100 | canvas.drawString(label, x + width/2, y + height - verticalPadding, regularTextPaint) 101 | } 102 | 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/scene/ui/Widget.scala: -------------------------------------------------------------------------------- 1 | //package sgl.scene 2 | //package ui 3 | // 4 | ///** SceneElement that is part of a layout 5 | // * 6 | // * This is the root class providing logic for 7 | // * how to render widgets in a hierarchical user 8 | // * interface. 9 | // */ 10 | //abstract class Widget(_x: Float, _y: Float) extends SceneElement(_x, _y) { 11 | // 12 | // def minWidth: Float 13 | // def minHeight: Float 14 | // 15 | // def preferredWidth: Float 16 | // def preferredHeight: Float 17 | // 18 | // def maxWidth: Option[Float] 19 | // def maxHeight: Option[Float] 20 | // 21 | // 22 | //} 23 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/AssertionsProvider.scala: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * This is a draft on an idea for an assertion module. 4 | * 5 | * An assertion provider should provide assert/require so 6 | * that we can use them whenever we assume that a condition should 7 | * always hold. Assertions are very useful concept for system with 8 | * complex state, such as games, because they let us clarify 9 | * our mind and catch errors early and thus simplify debugging. 10 | * 11 | * In release mode, there's an argument for not failing unless 12 | * we really have to, and maybe an unexpected state can still be 13 | * ok, which is why we would not want to crash on every assert even 14 | * if the state is corrupted. That should of course be balanced 15 | * with safety (for saved data) and there is always a risk of 16 | * corrupting player progress. My idea here is to provide an 17 | * assert and a fatalAssert, with the fatal would crash even 18 | * in a release mode, but the assert would not. 19 | * 20 | * The idea then is to have various possible implementations, the 21 | * default one would use built-in assert/requires, and just crash 22 | * with an exception. A release one would maybe do nothing. There 23 | * would be in-between solution, that would involve logging the 24 | * asserts but not crashing, or even better a crash report that 25 | * could be sent with all the asserts that failed. 26 | */ 27 | 28 | //trait AssertionsProvider { 29 | // 30 | // 31 | // trait Asserts { 32 | // 33 | // def assert() 34 | // 35 | // // crashes in all cases, even in release mode. 36 | // def fatal() 37 | // 38 | // } 39 | // val Asserts: Asserts 40 | // 41 | //} 42 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/ChunkedTask.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | /** A ChunkedTask is an expensive computation that can be split in small chunks. 4 | * 5 | * A ChunkedTask is typically an expensive computation that one would 6 | * like to execute in a thread because it would take too much time to 7 | * fit on on game loop update call. The idea is to break such computations 8 | * into small units, and execute each unit as part of calls to run. This 9 | * abstraction provides a way to spread the computation over several 10 | * iteration of the game loop, each time exploiting some remaining time after 11 | * the main updates. It can be scheduled with the SchedulerProvider. 12 | */ 13 | abstract class ChunkedTask { 14 | 15 | import ChunkedTask._ 16 | 17 | /* 18 | * There are two possible designs for an InterruptibleTask object. 19 | * 1) An execute(ms: Long) method that tell the task to execute for a given amount of ms milliseconds. 20 | * 2) A perform() method that tells the task to start executing and a pause() method that tells the task to stop. 21 | * However, I just realized that option 2) is kind of a no-go as it would require an external thread of control 22 | * to make the callback to stop, which obviously defeats the purpose of this task interface for platforms that 23 | * are singly-threaded (such as Javascript). 24 | */ 25 | 26 | /** A name used to identify the task, mostly for logging and debugging */ 27 | val name: String 28 | 29 | // TODO: Priority for scheduling? 30 | // val priority: Priority 31 | 32 | private var _status : Status = Pending 33 | def status: Status = _status 34 | 35 | /** Wrapper around the run method to be used internally. 36 | * 37 | * Method called by the scheduler, it will maintain 38 | * the internal state of the task and invoke the run method. 39 | * The run method should not be called directly. 40 | */ 41 | final private[sgl] def doRun(ms: Long): Unit = { 42 | _status = run(ms) 43 | assert(_status != Pending) 44 | } 45 | 46 | /** run this task for at most ms milliseconds. 47 | * 48 | * When the scheduler schedules a chunked task, the run 49 | * method will be invoked with some amount of milliseconds. 50 | * The way the scheduler decides how much time to give to a 51 | * task and which task to schedule is up to the exact implementation 52 | * of the scheduler. 53 | * 54 | * The task needs to respect the ms parameter and try as hard as 55 | * possible to stop when running out of time. It should also try 56 | * to maximize the use of this time as the game loop might 57 | * sleep for the remaining time after the return. The scheduler 58 | * will never enforce the ms constraint, as some implementation 59 | * might literally call the run method in the same thread of 60 | * execution (such as scalajs implementation for HTML5). 61 | * 62 | * Note that if the task does not return, it might block the whole 63 | * game loop and freeze the game. Although, if the platform provides 64 | * a threading system, it is also possible that ChunkedTask are run 65 | * in a thread and won't impact the main loop. 66 | * 67 | * There is no upper limit to the ms value, although most likely a 68 | * thread-based implementation is still going to pick some default 69 | * value and call run on the same task in an infinite loop. 70 | * 71 | * The method should return its status after this call to run, valid 72 | * values are either InProgress (if not completed) or Completed. The Pending 73 | * status is the default status when the task is still waiting to be 74 | * scheduled. 75 | */ 76 | protected def run(ms: Long): Status 77 | 78 | } 79 | 80 | object ChunkedTask { 81 | 82 | /** Status of a task. */ 83 | sealed abstract class Status 84 | /** The task is waiting to be scheduled. */ 85 | case object Pending extends Status 86 | /** The task is not done and the scheduler should schedule it again. */ 87 | case object InProgress extends Status 88 | /** The task is completed and the scheduler can disregard it. */ 89 | case object Completed extends Status 90 | 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/Math.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | object Math { 4 | 5 | val Pi = scala.math.Pi 6 | 7 | def degreeToRadian(degree: Double): Double = (degree/180d)*Pi 8 | def radianToDegree(radian: Double): Double = (radian*180d)/Pi 9 | 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/Pool.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import scala.collection.mutable.Queue 4 | 5 | /** Object pool utility. 6 | * 7 | * This can be used whenever we want to keep an object pool to be able 8 | * to re-use objects instead of re-allocating a lot which eventually leads 9 | * to garbage collection and freezing of the game. 10 | * 11 | * This is both ok for internal (in SGL) and external (in the game logic) 12 | * use. 13 | */ 14 | class Pool[T](create: () => T, size: Int) { 15 | private val pool: Queue[T] = Queue.fill(size)(create()) 16 | 17 | def acquire(): T = { 18 | if(pool.isEmpty) { 19 | create() 20 | } else { 21 | pool.dequeue() 22 | } 23 | } 24 | 25 | def release(obj: T): Unit = { 26 | pool.enqueue(obj) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/RandomProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package util 3 | 4 | /** An abstract provider for the Random object. 5 | * 6 | * Random is designed to be both simple to start using, by providing 7 | * a global Random object with a preset seed (randomly) ready to be used 8 | * out of the box. After mixing RandomProvider, one can simply start 9 | * calling Random.nextInt() to get some random number. Random also provides 10 | * some constructors fromSeed and newInstance to generate independent instances 11 | * of Random, in case one needs to maintain independent and parallel streams 12 | * of random number generations. 13 | */ 14 | trait RandomProvider { 15 | 16 | trait Random { 17 | 18 | def nextBoolean(): Boolean 19 | 20 | /** Return a uniformly distributed value between 0 (inclusive) and 1.0 (exclusive). */ 21 | def nextDouble(): Double 22 | 23 | /** Return a normally (gaussian) distributed value with mean 0 and standard deviation 1.0. */ 24 | def nextGaussian(): Double 25 | 26 | /** Return a uniformly distributed value between 0 (inclusive) and 1.0 (exclusive). */ 27 | def nextFloat(): Double 28 | 29 | /** Return a uniformly distributed value between 0 (inclusive) and n (exclusive). */ 30 | def nextInt(n: Int): Int 31 | 32 | /** Return a uniformly distributed Int value. */ 33 | def nextInt(): Int 34 | 35 | /** Return a uniformly distributed Long value. */ 36 | def nextLong(): Long 37 | 38 | /** Returns a uniformly distributed value between min (inclusive) and max (exclusive). */ 39 | def nextDouble(min: Double, max: Double): Double = { 40 | val diff = max - min 41 | min + nextDouble()*diff 42 | } 43 | 44 | /** Returns a uniformly distributed value between min (inclusive) and max (exclusive). */ 45 | def nextInt(min: Int, max: Int): Int = { 46 | val diff = max - min 47 | min + nextInt(diff) 48 | } 49 | 50 | /** Set the seed of the random generator. 51 | * 52 | * Once a seed is set, a random object will generate a deterministic 53 | * series of random values. If you would be to reset the same seed to 54 | * another instance of the Random object, you would get the same sequence 55 | * of random numbers. 56 | * 57 | * Setting the seed in the middle of a sequence of random generation will 58 | * reset the random generators to the same state as if it was just started 59 | * with the corresponding seed. 60 | */ 61 | def setSeed(seed: Long): Unit 62 | 63 | /** Create a new instance of Random from the seed. */ 64 | def fromSeed(seed: Long): Random = { 65 | val r = this.newInstance 66 | r.setSeed(seed) 67 | r 68 | } 69 | 70 | /** Create a new instance of Random. */ 71 | def newInstance: Random 72 | 73 | // TODO: maybe naming should be: uniformInt, uniformDouble, etc.., gaussianInt (if that makes some sort of sense?), gaussianDouble, etc .. ? 74 | // And then we could have more distributions too. 75 | // Also, to expose a seed or to not expose a seed? I would say to not, as this kind of replayability might not be necessary for games 76 | // and could potentially prevent some implementations of the random interface? 77 | 78 | } 79 | 80 | val Random: Random 81 | 82 | } 83 | 84 | trait DefaultRandomProvider extends RandomProvider { 85 | 86 | // java.util.Random seems to be supported by scalajs, scala-native, and is useable on Android. 87 | // We will keep the implementation simple and based on that for now, but apparently 88 | // the quality of random numbers is not great. That being said, for games, it might not be 89 | // as much of an issue as for security. We should eventually revisit if we need 90 | // a custom implementation for games. 91 | 92 | class JavaUtilRandomBasedRandom(random: java.util.Random) extends Random { 93 | override def nextBoolean(): Boolean = random.nextBoolean() 94 | override def nextDouble(): Double = random.nextDouble() 95 | override def nextGaussian(): Double = random.nextGaussian() 96 | override def nextFloat(): Double = random.nextFloat() 97 | override def nextInt(n: Int): Int = random.nextInt(n) 98 | override def nextInt(): Int = random.nextInt() 99 | override def nextLong(): Long = random.nextLong() 100 | 101 | override def setSeed(seed: Long): Unit = random.setSeed(seed) 102 | override def newInstance: Random = new JavaUtilRandomBasedRandom(new java.util.Random()) 103 | override def fromSeed(seed: Long): Random = new JavaUtilRandomBasedRandom(new java.util.Random(seed)) 104 | } 105 | override val Random: Random = new JavaUtilRandomBasedRandom(new java.util.Random()) 106 | 107 | } 108 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/SchedulerProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package util 3 | 4 | /** The provider for the platform-specific Scheduler. 5 | * 6 | * A scheduler should be unique per game and is platform 7 | * specific. This makes it a great candidate for being injected 8 | * by a Provider. 9 | */ 10 | trait SchedulerProvider { 11 | 12 | abstract class Scheduler { 13 | 14 | /** Register the task to be executed asynchronously. 15 | * 16 | * The task will be executed when the scheduler has available resources, 17 | * the details will depend on the scheduler implementation. 18 | */ 19 | def schedule(task: ChunkedTask): Unit 20 | 21 | } 22 | val Scheduler: Scheduler 23 | 24 | } 25 | 26 | /** This is a default implementation of the SchedulerProvider 27 | * that does not rely on any platform-specific features. It is able 28 | * to execute tasks by allocating CPU from the main game loop 29 | * when invoked by the run() method. 30 | */ 31 | trait SingleThreadSchedulerProvider extends SchedulerProvider { 32 | this: LoggingProvider with SystemProvider => 33 | 34 | import scala.collection.mutable.Queue 35 | 36 | private implicit val Tag = Logger.Tag("single-thread-scheduler") 37 | 38 | // For obvious reasons, this Scheduler is not thread-safe. It should 39 | // always be called from the game loop thread and never from another 40 | // place. 41 | // This scheduler is also simple and fair, just giving up-to 5ms to each 42 | // of the task in the queue, in order. More advanced implementation of the 43 | // Scheduler interface could take into account priorities or more advanced 44 | // things. 45 | class SingleThreadScheduler extends Scheduler { 46 | 47 | // TODO: this is actually not very fair, as if run is called with a non-multiple 48 | // of 5ms, the last task to be scheduled will only get the remainder (<5) and 49 | // then be re-enqueued at the end of the queue. 50 | 51 | private val taskQueue: Queue[ChunkedTask] = new Queue 52 | 53 | override def schedule(task: ChunkedTask): Unit = { 54 | taskQueue.enqueue(task) 55 | } 56 | 57 | /** Give control to the scheduler to allocate 58 | * CPU to the pending tasks. The scheduler returns 59 | * either after the ms amount of time, or as soon 60 | * as no more work is required. 61 | * 62 | * Return true if the task queue is empty. 63 | */ 64 | def run(ms: Long): Boolean = { 65 | logger.trace("Running SingleThreadScheduler with taskQueue size of: " + taskQueue.size) 66 | val endTime = System.nanoTime + ms*1000L*1000L 67 | var remaining = endTime - System.nanoTime 68 | while(remaining > 0 && taskQueue.nonEmpty) { 69 | val available = (remaining/(1000L*1000L)) min 5 70 | val task = taskQueue.dequeue() 71 | task.doRun(available) 72 | if(task.status == ChunkedTask.InProgress) 73 | taskQueue.enqueue(task) 74 | remaining = endTime - System.nanoTime 75 | } 76 | taskQueue.isEmpty 77 | } 78 | 79 | } 80 | override val Scheduler = new SingleThreadScheduler 81 | } 82 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/metrics/Counter.scala: -------------------------------------------------------------------------------- 1 | package sgl.util.metrics 2 | 3 | class Counter(_name: String) extends Metrics(_name) { 4 | 5 | private var c: Int = 0 6 | 7 | def incr(): Unit = { 8 | c += 1 9 | } 10 | 11 | def add(amount: Int): Unit = { 12 | require(amount >= 0) 13 | c += amount 14 | } 15 | 16 | def += (amount: Int): Unit = add(amount) 17 | 18 | def get: Int = c 19 | 20 | override def reset(): Unit = { 21 | c = 0 22 | } 23 | 24 | override def toString: String = s"$name => $c" 25 | 26 | override def renderString: String = s"$name $c" 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/metrics/Gauge.scala: -------------------------------------------------------------------------------- 1 | package sgl.util.metrics 2 | 3 | class FloatGauge(_name: String) extends Metrics(_name) { 4 | 5 | private var v: Float = 0 6 | 7 | def add(x: Float): Unit = { 8 | v += x 9 | } 10 | def += (x: Float): Unit = add(x) 11 | 12 | def set(x: Int): Unit = { 13 | v = x 14 | } 15 | 16 | def get: Float = v 17 | 18 | override def reset(): Unit = { 19 | v = 0 20 | } 21 | 22 | override def toString: String = s"$name => $v" 23 | 24 | override def renderString: String = s"$name $v" 25 | } 26 | 27 | 28 | class IntGauge(_name: String) extends Metrics(_name) { 29 | 30 | private var v: Int = 0 31 | 32 | def add(x: Int): Unit = { 33 | v += x 34 | } 35 | def += (x: Int): Unit = add(x) 36 | 37 | def set(x: Int): Unit = { 38 | v = x 39 | } 40 | 41 | def get: Int = v 42 | 43 | override def reset(): Unit = { 44 | v = 0 45 | } 46 | 47 | override def toString: String = s"$name => $v" 48 | 49 | override def renderString: String = s"$name $v" 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/metrics/Histogram.scala: -------------------------------------------------------------------------------- 1 | package sgl.util.metrics 2 | 3 | class Histogram(_name: String, buckets: Array[Float]) extends Metrics(_name) { 4 | 5 | // buckets is a list of upper bounds, with implicit -inf and +inf at 6 | // both ends. That is, given [a1, a2, a3], the implicit buckets defineds 7 | // are: ]-inf, a1], ]a1, a2], ]a2, a3], ]a3, +inf[. 8 | for(i <- 0 until (buckets.size-1)) { 9 | require(buckets(i) < buckets(i+1)) 10 | } 11 | 12 | // Keep a current count for each bucket. Length is buckets.size + 1, with 13 | // the last element being all the values larger that the last element of the 14 | // bucket. 15 | private val counts: Array[Int] = buckets.map(_ => 0) ++ Array(0) 16 | private var sum = 0f 17 | private var c = 0 18 | 19 | /** Add an observation to the histogram. */ 20 | def observe(v: Float): Unit = { 21 | c += 1 22 | sum += v 23 | 24 | var i = 0 25 | while(i < buckets.length) { 26 | if(v <= buckets(i)) { 27 | counts(i) += 1 28 | return 29 | } 30 | i += 1 31 | } 32 | 33 | // If we haven't found any, we count for the last bucket. 34 | counts(buckets.length) += 1 35 | } 36 | 37 | // Number of observation so far. 38 | def count: Int = c 39 | 40 | // Total sum of all observations so far. 41 | def totalSum: Float = sum 42 | 43 | def average: Float = totalSum / count 44 | 45 | override def reset(): Unit = { 46 | var i = 0 47 | while(i < counts.length) { 48 | counts(i) = 0 49 | i += 1 50 | } 51 | 52 | sum = 0 53 | c = 0 54 | } 55 | 56 | // TODO: some form of percentile could be nice? 57 | //def percentile(n: Int): Float = ??? 58 | 59 | 60 | // TODO: would be nice if we could provide a time function that adds an observation: 61 | //def time(body: =>Unit): Unit = ??? 62 | 63 | override def toString: String = { 64 | s"$name\naverage=${this.average}\nmedian=???\n" + 65 | counts.zipWithIndex.filter(_._1 != 0).map{ case (c, i) => { 66 | val from = if(i == 0) "-inf" else buckets(i-1) 67 | val to = if(i == buckets.size) "+inf" else buckets(i) 68 | s"]$from,$to] -> $c" 69 | }}.mkString("\n") 70 | } 71 | 72 | override def renderString: String = { 73 | "%s %.4f (average in ms/s/m TODO)".format(name, average) 74 | } 75 | 76 | } 77 | 78 | object Histogram { 79 | 80 | /** Create a histogram with linear buckets. 81 | * 82 | * The buckets are starting from from until to, with count 83 | * steps between them. The first bucket is ]-inf, from], and 84 | * the last one is ]to, +inf[. In addition to these two 85 | * implicit buckets, there will be count buckets for 86 | * intermediate deltas. 87 | */ 88 | def linear(name: String, from: Float, to: Float, count: Int): Histogram = { 89 | val delta: Float = (to - from) / count 90 | val buckets = for(i <- 0 to count) yield (from + i*delta) 91 | new Histogram(name, buckets.toArray) 92 | } 93 | 94 | // TODO: with explicit buckets 95 | //def withBuckets(buckets: Array[Float] 96 | 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/scala/sgl/util/metrics/Metrics.scala: -------------------------------------------------------------------------------- 1 | package sgl.util.metrics 2 | 3 | abstract class Metrics(val name: String) { 4 | 5 | /** Return a string representation of the metrics that can be rendered. */ 6 | def renderString: String 7 | 8 | /** Reset the metrics to its default value. */ 9 | def reset(): Unit 10 | 11 | } 12 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/GraphicsHelpersSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class GraphicsHelperSuite extends AnyFunSuite { 6 | 7 | val graphicsProvider = new TestGraphicsProvider with TestSystemProvider {} 8 | 9 | test("BitmapRegion with single bitmap") { 10 | import graphicsProvider.Graphics._ 11 | 12 | val testBitmap = new TestBitmap { 13 | override def height = 24 14 | override def width = 32 15 | } 16 | val br = BitmapRegion(testBitmap) 17 | assert(br.bitmap === testBitmap) 18 | assert(br.x === 0) 19 | assert(br.y === 0) 20 | assert(br.width === 32) 21 | assert(br.height === 24) 22 | } 23 | 24 | test("BitmapRegion split of a bitmap") { 25 | import graphicsProvider.Graphics._ 26 | 27 | val testBitmap = new TestBitmap { 28 | override def height = 64 29 | override def width = 90 30 | } 31 | val brs = BitmapRegion.split(testBitmap, 0, 0, 30, 32, 3, 2) 32 | 33 | assert(brs.size === 6) 34 | 35 | assert(brs(0).bitmap === testBitmap) 36 | assert(brs(0).x === 0) 37 | assert(brs(0).y === 0) 38 | assert(brs(0).width === 30) 39 | assert(brs(0).height === 32) 40 | 41 | assert(brs(1).bitmap === testBitmap) 42 | assert(brs(1).x === 30) 43 | assert(brs(1).y === 0) 44 | assert(brs(1).width === 30) 45 | assert(brs(1).height === 32) 46 | 47 | assert(brs(2).bitmap === testBitmap) 48 | assert(brs(2).x === 60) 49 | assert(brs(2).y === 0) 50 | assert(brs(2).width === 30) 51 | assert(brs(2).height === 32) 52 | 53 | assert(brs(3).bitmap === testBitmap) 54 | assert(brs(3).x === 0) 55 | assert(brs(3).y === 32) 56 | assert(brs(3).width === 30) 57 | assert(brs(3).height === 32) 58 | 59 | assert(brs(4).bitmap === testBitmap) 60 | assert(brs(4).x === 30) 61 | assert(brs(4).y === 32) 62 | assert(brs(4).width === 30) 63 | assert(brs(4).height === 32) 64 | 65 | assert(brs(5).bitmap === testBitmap) 66 | assert(brs(5).x === 60) 67 | assert(brs(5).y === 32) 68 | assert(brs(5).width === 30) 69 | assert(brs(5).height === 32) 70 | 71 | val brs2 = BitmapRegion.split(testBitmap, 30, 0, 30, 32, 1, 2) 72 | assert(brs2.size === 2) 73 | assert(brs2(0).bitmap === testBitmap) 74 | assert(brs2(0).x === 30) 75 | assert(brs2(0).y === 0) 76 | assert(brs2(0).width === 30) 77 | assert(brs2(0).height === 32) 78 | assert(brs2(1).bitmap === testBitmap) 79 | assert(brs2(1).x === 30) 80 | assert(brs2(1).y === 32) 81 | assert(brs2(1).width === 30) 82 | assert(brs2(1).height === 32) 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/SaveComponentSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class SaveComponentSuite extends AnyFunSuite { 6 | 7 | test("MemorySaveComponent is working") { 8 | val save = new MemorySaveComponent {} 9 | import save.Save 10 | 11 | assert(Save.getInt("a") === None) 12 | assert(Save.getIntOrElse("a", 13) === 13) 13 | assert(Save.getIntOrElse("a", 15) === 15) 14 | assert(Save.getInt("a") === None) 15 | 16 | Save.putInt("a", 42) 17 | assert(Save.getInt("a") === Some(42)) 18 | assert(Save.getIntOrElse("a", 13) === 42) 19 | assert(Save.getInt("b") === None) 20 | } 21 | 22 | 23 | test("Testing SavedValue") { 24 | var getIntCalled = false 25 | val save = new MemorySaveComponent { 26 | override val Save = new MemorySave { 27 | override def getIntOrElse(name: String, default: Int): Int = { 28 | getIntCalled = true 29 | super.getIntOrElse(name, default) 30 | } 31 | } 32 | } 33 | 34 | val value = new save.IntSavedValue("a", 42) 35 | 36 | assert(!getIntCalled) 37 | assert(value.get === 42) 38 | assert(getIntCalled) // Initial get call require to read the store to check if anything was persisted. 39 | assert(save.Save.getInt("a") === None) // We don't persist the default value, so it should still be None. 40 | 41 | getIntCalled = false // Reset getIntCalled. As of now we should be using the cache so no more get needed. 42 | 43 | // Let's check that the default is gotten from memory only. 44 | assert(value.get === 42) 45 | assert(!getIntCalled) 46 | 47 | value.put(13) 48 | assert(!getIntCalled) // put does not need to call get 49 | assert(value.get == 13) 50 | assert(!getIntCalled) // get should use the cached value 51 | assert(save.Save.getInt("a") === Some(13)) // Check that the right value is persisted. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/SystemProviderSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class SystemProviderSuite extends AnyFunSuite { 6 | 7 | object InstrumentedSystemProvider extends TestSystemProvider { 8 | var instrumentedUri: java.net.URI = null 9 | class InstrumentedTestSystem extends TestSystem { 10 | override def openWebpage(uri: java.net.URI): Unit = { 11 | instrumentedUri = uri 12 | } 13 | } 14 | override val System = new InstrumentedTestSystem 15 | } 16 | 17 | test("openGooglePlayApp defaults to the correct URL without parameters") { 18 | InstrumentedSystemProvider.System.openGooglePlayApp("com.regblanc.sgl") 19 | val want = new java.net.URI("https://play.google.com/store/apps/details?id=com.regblanc.sgl") 20 | assert(InstrumentedSystemProvider.instrumentedUri == want) 21 | } 22 | 23 | test("openGooglePlayApp defaults to the correct URL with parameters") { 24 | InstrumentedSystemProvider.System.openGooglePlayApp("com.regblanc.sgl", Map("a" -> "b")) 25 | val want = new java.net.URI("https://play.google.com/store/apps/details?id=com.regblanc.sgl&a=b") 26 | assert(InstrumentedSystemProvider.instrumentedUri == want) 27 | InstrumentedSystemProvider.System.openGooglePlayApp("com.regblanc.sgl", Map("a" -> "b", "c" -> "d")) 28 | val want2 = new java.net.URI("https://play.google.com/store/apps/details?id=com.regblanc.sgl&a=b&c=d") 29 | assert(InstrumentedSystemProvider.instrumentedUri == want2) 30 | } 31 | 32 | 33 | object PartsResourcePathSystemProvider extends TestSystemNoResourcePathProvider with PartsResourcePathProvider { 34 | override val ResourcesRoot = PartsResourcePath(Vector("root")) 35 | override val MultiDPIResourcesRoot = PartsResourcePath(Vector("root")) 36 | } 37 | 38 | test("PartsResourcePath creates the correct path") { 39 | val r = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "b" / "c.txt" 40 | assert(r.path === "root/a/b/c.txt") 41 | val r2 = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "b/c.txt" 42 | assert(r2.path === "root/a/b/c.txt") 43 | } 44 | 45 | test("PartsResourcePath creates the correct pathes with multiple filenames") { 46 | val rs = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "b" / Seq("c.txt", "d.txt") 47 | assert(rs.size === 2) 48 | assert(rs(0).path === "root/a/b/c.txt") 49 | assert(rs(1).path === "root/a/b/d.txt") 50 | } 51 | 52 | test("PartsResourcePath returns proper extension") { 53 | val r = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "b" / "c.txt" 54 | assert(r.extension === Some("txt")) 55 | val r2 = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "b" / "c" 56 | assert(r2.extension === None) 57 | } 58 | 59 | test("PartsResourcePath creates the correct path with .") { 60 | val r = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "." / "b" / "c.txt" 61 | assert(r.path === "root/a/b/c.txt") 62 | val r2 = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "./b/c.txt" 63 | assert(r2.path === "root/a/b/c.txt") 64 | } 65 | 66 | test("PartsResourcePath creates the correct path with ..") { 67 | val r = PartsResourcePathSystemProvider.ResourcesRoot / "a" / ".." / "b" / "c.txt" 68 | assert(r.path === "root/b/c.txt") 69 | val r2 = PartsResourcePathSystemProvider.ResourcesRoot / ".." / "a" / "b" / "c.txt" 70 | assert(r2.path === "a/b/c.txt") 71 | val r3 = PartsResourcePathSystemProvider.ResourcesRoot / "a" / "../b/c.txt" 72 | assert(r3.path === "root/b/c.txt") 73 | val r4 = PartsResourcePathSystemProvider.ResourcesRoot / "../a/b/c.txt" 74 | assert(r4.path === "a/b/c.txt") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/TestGraphicsProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import sgl.util.Loader 4 | 5 | // TODO: we should add some internal state, which will make this useable for 6 | // mocking and testing that expected functions have been called. Maybe we can 7 | // have a true pixel array and have functions to query internal state. 8 | 9 | trait TestGraphicsProvider extends GraphicsProvider { 10 | this: SystemProvider => 11 | 12 | class TestGraphics extends Graphics { 13 | 14 | class TestBitmap extends AbstractBitmap { 15 | override def height: Int = ??? 16 | override def width: Int = ??? 17 | 18 | override def release(): Unit = {} 19 | } 20 | type Bitmap = TestBitmap 21 | override def loadImage(path: ResourcePath): Loader[Bitmap] = ??? 22 | 23 | class TestFont extends AbstractFont { 24 | override def size: Int = ??? 25 | override def withSize(size: Int): Font = ??? 26 | override def withStyle(style: Font.Style): Font = ??? 27 | override def isBold(): Boolean = ??? 28 | override def isItalic(): Boolean = ??? 29 | } 30 | type Font = TestFont 31 | class TestFontCompanion extends FontCompanion { 32 | def create(family: String, style: Style, size: Int): Font = ??? 33 | def load(path: ResourcePath): Loader[Font] = ??? 34 | val Default: Font = new TestFont 35 | val DefaultBold: Font = new TestFont 36 | val Monospace: Font = new TestFont 37 | val SansSerif: Font = new TestFont 38 | val Serif: Font = new TestFont 39 | } 40 | val Font = new TestFontCompanion 41 | 42 | type Color = Int 43 | class TestColorCompanion extends ColorCompanion { 44 | def rgb(r: Int, g: Int, b: Int): Color = ??? 45 | def rgba(r: Int, g: Int, b: Int, a: Int): Color = ??? 46 | } 47 | val Color = new TestColorCompanion 48 | 49 | class TestPaint extends AbstractPaint { 50 | def font: Font = ??? 51 | def withFont(font: Font): Paint = ??? 52 | def color: Color = ??? 53 | def withColor(color: Color): Paint = ??? 54 | def alignment: Alignments.Alignment = ??? 55 | def withAlignment(alignment: Alignments.Alignment): Paint = ??? 56 | } 57 | type Paint = TestPaint 58 | def defaultPaint: Paint = ??? 59 | 60 | class TestTextLayout extends AbstractTextLayout { 61 | def height: Int = ??? 62 | } 63 | type TextLayout = TestTextLayout 64 | 65 | class TestCanvas extends AbstractCanvas { 66 | 67 | def width: Float = ??? 68 | def height: Float = ??? 69 | 70 | override def withSave[A](body: => A): A = ??? 71 | override def translate(x: Float, y: Float): Unit = ??? 72 | override def rotate(theta: Float): Unit = ??? 73 | override def scale(sx: Float, sy: Float): Unit = ??? 74 | override def clipRect(x: Float, y: Float, width: Float, height: Float): Unit = ??? 75 | 76 | override def drawBitmap(bitmap: Bitmap, dx: Float, dy: Float, dw: Float, dh: Float, sx: Int, sy: Int, sw: Int, sh: Int, alpha: Float): Unit = ??? 77 | 78 | override def drawRect(x: Float, y: Float, width: Float, height: Float, paint: Paint): Unit = ??? 79 | 80 | override def drawOval(x: Float, y: Float, width: Float, height: Float, paint: Paint): Unit = ??? 81 | override def drawLine(x1: Float, y1: Float, x2: Float, y2: Float, paint: Paint): Unit = ??? 82 | 83 | override def drawString(str: String, x: Float, y: Float, paint: Paint): Unit = ??? 84 | override def drawText(text: TextLayout, x: Float, y: Float): Unit = ??? 85 | override def renderText(text: String, width: Int, paint: Paint): TextLayout = ??? 86 | } 87 | type Canvas = TestCanvas 88 | } 89 | override val Graphics = new TestGraphics 90 | 91 | } 92 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/TestSystemProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | 3 | import sgl.util._ 4 | 5 | import scala.util._ 6 | 7 | trait TestSystemProvider extends TestSystemNoResourcePathProvider { 8 | 9 | class TestResourcePath extends AbstractResourcePath { 10 | def / (filename: String): ResourcePath = ??? 11 | 12 | override def extension: Option[String] = ??? 13 | } 14 | type ResourcePath = TestResourcePath 15 | override val ResourcesRoot: ResourcePath = new TestResourcePath 16 | override val MultiDPIResourcesRoot: ResourcePath = new TestResourcePath 17 | 18 | } 19 | 20 | trait TestSystemNoResourcePathProvider extends SystemProvider { 21 | 22 | class TestSystem extends System { 23 | 24 | def exit(): Unit = ??? 25 | def millis(): Long = ??? 26 | 27 | def currentTimeMillis: Long = ??? 28 | def nanoTime: Long = ??? 29 | 30 | def loadText(path: ResourcePath): Loader[Array[String]] = ??? 31 | 32 | def loadBinary(path: ResourcePath): Loader[Array[Byte]] = ??? 33 | 34 | def openWebpage(uri: java.net.URI): Unit = ??? 35 | 36 | } 37 | val System = new TestSystem 38 | 39 | } 40 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/geometry/CircleSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class CircleSuite extends AnyFunSuite { 6 | 7 | test("adds a Vec") { 8 | val c = Circle(0, 0, 10) 9 | val v = Vec(1, 1) 10 | val expected = Circle(1,1,10) 11 | assert(c + v === expected) 12 | } 13 | 14 | test("subtracts a Vec") { 15 | val c = Circle(0, 0, 10) 16 | val v = Vec(1, 1) 17 | val expected = Circle(-1,-1,10) 18 | assert(c - v === expected) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/geometry/CollisionsSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class CollisionsSuite extends AnyFunSuite { 6 | 7 | test("polygonWithPolygonSat with simple rects") { 8 | val p1 = Polygon(Vector(Vec(0,0), Vec(0, 10), Vec(10, 10), Vec(10, 0))) 9 | val p2 = Polygon(Vector(Vec(5,5), Vec(5, 15), Vec(15, 15), Vec(15, 5))) 10 | assert(Collisions.polygonWithPolygonSat(p1, p2)) 11 | assert(Collisions.polygonWithPolygonSat(p2, p1)) 12 | 13 | val p3 = Polygon(Vector(Vec(15,15), Vec(15, 20), Vec(20, 20), Vec(20, 15))) 14 | assert(!Collisions.polygonWithPolygonSat(p1, p3)) 15 | assert(!Collisions.polygonWithPolygonSat(p3, p1)) 16 | } 17 | 18 | test("polygonWithPolygonSat with triangles") { 19 | val p1 = Polygon(Vector(Vec(0,0), Vec(2, 10), Vec(8, 4))) 20 | val p2 = Polygon(Vector(Vec(4,4), Vec(10, 10), Vec(11, 1))) 21 | assert(Collisions.polygonWithPolygonSat(p1, p2)) 22 | assert(Collisions.polygonWithPolygonSat(p2, p1)) 23 | 24 | val p3 = Polygon(Vector(Vec(12,0), Vec(13, 8), Vec(17, 4.5f))) 25 | assert(!Collisions.polygonWithPolygonSat(p1, p3)) 26 | assert(!Collisions.polygonWithPolygonSat(p3, p1)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/geometry/EllipseSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class EllipseSuite extends AnyFunSuite { 6 | 7 | test("adds a Vec") { 8 | val c = Ellipse(0, 0, 10, 20) 9 | val v = Vec(1, 1) 10 | val expected = Ellipse(1,1,10, 20) 11 | assert(c + v === expected) 12 | } 13 | 14 | test("subtracts a Vec") { 15 | val c = Ellipse(0, 0, 10, 20) 16 | val v = Vec(1, 1) 17 | val expected = Ellipse(-1,-1,10, 20) 18 | assert(c - v === expected) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/geometry/PolygonSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class PolygonSuite extends AnyFunSuite { 6 | 7 | test("Create polygon with correct vertices and edges") { 8 | val p = Polygon(Vector(Vec(0,0), Vec(1, 10), Vec(5, 5))) 9 | assert(p.vertices(0) === Vec(0,0)) 10 | assert(p.vertices(1) === Vec(1,10)) 11 | assert(p.vertices(2) === Vec(5,5)) 12 | assert(p.nbEdges === 3) 13 | assert(p.edgeStart(0) === Vec(0,0)) 14 | assert(p.edgeEnd(0) === Vec(1,10)) 15 | assert(p.edgeStart(1) === Vec(1,10)) 16 | assert(p.edgeEnd(1) === Vec(5,5)) 17 | assert(p.edgeStart(2) === Vec(5,5)) 18 | assert(p.edgeEnd(2) === Vec(0,0)) 19 | } 20 | 21 | test("Correct bounding box for polygon") { 22 | val p = Polygon(Vector(Vec(0, 5), Vec(3, 8), Vec(7, 4), Vec(4, -2))) 23 | val bb = p.boundingBox 24 | assert(bb.top == -2) 25 | assert(bb.left == 0) 26 | assert(bb.right == 7) 27 | assert(bb.bottom == 8) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/geometry/RectSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.geometry 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class RectSuite extends AnyFunSuite { 6 | 7 | test("Create rectangle with correct coordinates, dimensions, and center.") { 8 | val r1 = Rect(4, 7, 10, 20) 9 | assert(r1.left === 4) 10 | assert(r1.top === 7) 11 | assert(r1.width === 10) 12 | assert(r1.height === 20) 13 | assert(r1.right === 14) 14 | assert(r1.bottom === 27) 15 | assert(r1.centerX === 9) 16 | assert(r1.centerY === 17) 17 | } 18 | 19 | test("Rectangle intesects with a point") { 20 | val r1 = Rect(0, 0, 10, 20) 21 | assert(r1.intersect(2, 3)) 22 | assert(r1.intersect(5, 15)) 23 | assert(!r1.intersect(11, 10)) 24 | assert(!r1.intersect(5, 25)) 25 | } 26 | 27 | test("adds a Vec") { 28 | val r = Rect(0,0,10,10) 29 | val v = Vec(1,2) 30 | val expected = Rect(1,2,10,10) 31 | val result = r + v 32 | assert(result.left === expected.left) 33 | assert(result.top === expected.top) 34 | assert(result.width === expected.width) 35 | assert(result.height === expected.height) 36 | } 37 | 38 | test("subtracts a Vec") { 39 | val r = Rect(0,0,10,10) 40 | val v = Vec(1,2) 41 | val expected = Rect(-1,-2,10,10) 42 | val result = r - v 43 | assert(result.left === expected.left) 44 | assert(result.top === expected.top) 45 | assert(result.width === expected.width) 46 | assert(result.height === expected.height) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/util/DefaultLoaderSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | import scala.util.{Success, Failure} 6 | 7 | class DefaultLoaderSuite extends AnyFunSuite { 8 | 9 | // TODO: with LoaderAbstractSuite { 10 | // Try to run all the abstract loader suite test, but they don't seem to work 11 | // due to the sleep behavior. 12 | // override def makeLoader[A](body: => A): Loader[A] = { 13 | // val p = new DefaultLoader[A] 14 | // try { 15 | // p.success(body) 16 | // } catch { 17 | // case (e: Exception) => p.failure(e) 18 | // } 19 | // p.loader 20 | // } 21 | 22 | test("successful returns a loaded Loader with correct content") { 23 | val l = Loader.successful(13) 24 | assert(l.isLoaded) 25 | assert(l.value.get.get === 13) 26 | } 27 | 28 | test("failed returns a loaded Loader with correct content") { 29 | val l = Loader.failed[Int](new RuntimeException) 30 | assert(l.isLoaded) 31 | assert(l.value.get.isInstanceOf[Failure[Int]]) 32 | } 33 | 34 | test("combine of successful loaders returns a loaded loader with correct content") { 35 | val l1 = Loader.successful(1) 36 | val l2 = Loader.successful(2) 37 | val l3 = Loader.successful(3) 38 | 39 | val l = Loader.combine(Seq(l1, l2, l3)) 40 | 41 | assert(l.isLoaded) 42 | assert(l.value.get.get === Seq(1,2,3)) 43 | } 44 | 45 | test("combine with one failed loader returns a loaded loader with a failure") { 46 | val l1 = Loader.successful(1) 47 | val l2 = Loader.successful(2) 48 | val l3 = Loader.failed(new RuntimeException) 49 | 50 | val l = Loader.combine(Seq(l1, l2, l3)) 51 | 52 | assert(l.isLoaded) 53 | assert(l.value.get.isInstanceOf[Failure[Seq[Int]]]) 54 | } 55 | 56 | test("combine with multiple failed loaders returns a loaded loader with a failure") { 57 | val l1 = Loader.successful(1) 58 | val l2 = Loader.failed(new RuntimeException) 59 | val l3 = Loader.failed(new RuntimeException) 60 | 61 | val l = Loader.combine(Seq(l1, l2, l3)) 62 | 63 | assert(l.isLoaded) 64 | assert(l.value.get.isInstanceOf[Failure[Seq[Int]]]) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/util/LoaderAbstractSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | import scala.util.{Success, Failure} 6 | 7 | trait LoaderAbstractSuite extends AnyFunSuite { 8 | 9 | def makeLoader[A](body: => A): Loader[A] 10 | 11 | /* 12 | * TODO: How to replace the Thread.sleep in these tests with something more 13 | * predictable and cross-platform? 14 | */ 15 | 16 | test("onLoad is called when and only when completed") { 17 | val l = makeLoader({ 18 | Thread.sleep(200) 19 | }) 20 | 21 | var called = false 22 | l.onLoad(_ => { 23 | called = true 24 | }) 25 | 26 | Thread.sleep(100) 27 | assert(!called) 28 | assert(!l.isLoaded) 29 | Thread.sleep(200) 30 | assert(called) 31 | assert(l.isLoaded) 32 | } 33 | 34 | test("transform is called with the right result") { 35 | val l = makeLoader({ 36 | Thread.sleep(200) 37 | 42 38 | }) 39 | 40 | var called = false 41 | val l2 = l.transform{ 42 | case f@Failure(_) => { 43 | assert(false) 44 | f 45 | } 46 | case Success(n) => { 47 | called = true 48 | assert(n === 42) 49 | Success(n+1) 50 | } 51 | } 52 | 53 | Thread.sleep(100) 54 | assert(!called) 55 | assert(!l2.isLoaded) 56 | Thread.sleep(200) 57 | assert(called) 58 | assert(l2.isLoaded) 59 | assert(l2.value.get.get === 43) 60 | } 61 | 62 | test("transformWith is called with the right result") { 63 | val l = makeLoader({ 64 | Thread.sleep(200) 65 | 42 66 | }) 67 | 68 | var called = false 69 | val l2 = l.transformWith{ 70 | case f@Failure(_) => { 71 | assert(false) 72 | ??? 73 | } 74 | case Success(n) => { 75 | called = true 76 | assert(n === 42) 77 | makeLoader({ 78 | Thread.sleep(300) 79 | n+1 80 | }) 81 | } 82 | } 83 | 84 | Thread.sleep(100) 85 | assert(!called) 86 | 87 | Thread.sleep(200) 88 | assert(called) 89 | assert(l.isLoaded) 90 | assert(!l2.isLoaded) 91 | 92 | Thread.sleep(400) 93 | assert(l2.isLoaded) 94 | assert(l2.value.get.get === 43) 95 | } 96 | 97 | test("fallbackTo returns the first successful value and ignore follow up loaders") { 98 | val l1 = makeLoader(42) 99 | 100 | var called = false 101 | val l2 = l1.fallbackTo(makeLoader({ 102 | called = true 103 | 10 104 | })) 105 | Thread.sleep(200) 106 | assert(l2.isLoaded) 107 | assert(l2.value.get.get === 42) 108 | assert(!called) 109 | 110 | 111 | // If we create the loader outside fallbackTo, it will start executing anyway. 112 | called = false 113 | val l3 = makeLoader({ 114 | called = true 115 | 10 116 | }) 117 | val l4 = l1 fallbackTo l3 118 | Thread.sleep(200) 119 | assert(called) 120 | assert(l4.isLoaded) 121 | assert(l4.value.get.get === 42) 122 | } 123 | 124 | test("fallbackTo fallbacks to a successful value after a failed loader") { 125 | val l1 = makeLoader(throw new Exception("failed loader")) 126 | val l2 = l1.fallbackTo(makeLoader(10)) 127 | Thread.sleep(100) 128 | assert(l2.isLoaded) 129 | assert(l2.value.get.get === 10) 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/util/RandomProviderSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | trait RandomProviderAbstractSuite extends AnyFunSuite with RandomProvider { 6 | 7 | test("Two instances from same seed produces same stream of random data") { 8 | val r1 = Random.fromSeed(77) 9 | val r2 = Random.fromSeed(77) 10 | assert(r1.nextInt() === r2.nextInt()) 11 | assert(r1.nextInt() === r2.nextInt()) 12 | assert(r1.nextLong() === r2.nextLong()) 13 | } 14 | 15 | test("A Random instance that reset the seed reproduces the same stream of random data") { 16 | val r = Random.fromSeed(12) 17 | val n1 = r.nextInt() 18 | val n2 = r.nextInt() 19 | val n3 = r.nextLong() 20 | r.setSeed(12) 21 | assert(n1 === r.nextInt()) 22 | assert(n2 === r.nextInt()) 23 | assert(n3 === r.nextLong()) 24 | } 25 | 26 | } 27 | 28 | class DefaultRandomProviderSuite extends RandomProviderAbstractSuite with DefaultRandomProvider 29 | -------------------------------------------------------------------------------- /core/src/test/scala/sgl/util/metrics/MetricsSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.util.metrics 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class MetricsSuite extends AnyFunSuite { 6 | 7 | test("Counter is correctly initialized") { 8 | val c = new Counter("name") 9 | assert(c.name === "name") 10 | assert(c.get === 0) 11 | } 12 | 13 | test("Counter incr correct values") { 14 | val c = new Counter("name") 15 | c.incr() 16 | assert(c.get === 1) 17 | c.incr() 18 | assert(c.get === 2) 19 | } 20 | test("Counter adds correct values") { 21 | val c = new Counter("name") 22 | c.add(1) 23 | assert(c.get === 1) 24 | c.add(3) 25 | assert(c.get === 4) 26 | c += 2 27 | assert(c.get === 6) 28 | } 29 | test("Counter cannot add negative values") { 30 | val c = new Counter("name") 31 | intercept[IllegalArgumentException] { 32 | c.add(-1) 33 | } 34 | } 35 | 36 | test("IntGauge is correctly initialized") { 37 | val g = new IntGauge("name") 38 | assert(g.name === "name") 39 | assert(g.get === 0) 40 | } 41 | test("IntGauge adds correct values") { 42 | val g = new IntGauge("name") 43 | g.add(1) 44 | assert(g.get === 1) 45 | g.add(3) 46 | assert(g.get === 4) 47 | g += 2 48 | assert(g.get === 6) 49 | } 50 | test("IntGauge can go negative") { 51 | val g = new IntGauge("name") 52 | g.add(-2) 53 | assert(g.get === -2) 54 | } 55 | test("IntGauge can set arbitrary value") { 56 | val g = new IntGauge("name") 57 | g.set(2) 58 | assert(g.get === 2) 59 | g.set(1) 60 | assert(g.get === 1) 61 | } 62 | 63 | test("FloatGauge is correctly initialized") { 64 | val g = new FloatGauge("name") 65 | assert(g.name === "name") 66 | assert(g.get === 0) 67 | } 68 | test("FloatGauge adds correct values") { 69 | val g = new FloatGauge("name") 70 | g.add(1) 71 | assert(g.get === 1) 72 | g.add(3) 73 | assert(g.get === 4) 74 | g += 2 75 | assert(g.get === 6) 76 | } 77 | test("FloatGauge can go negative") { 78 | val g = new FloatGauge("name") 79 | g.add(-2) 80 | assert(g.get === -2) 81 | } 82 | test("FloatGauge can set arbitrary value") { 83 | val g = new FloatGauge("name") 84 | g.set(2) 85 | assert(g.get === 2) 86 | g.set(1) 87 | assert(g.get === 1) 88 | } 89 | 90 | test("Histogram is correctly initialized") { 91 | val h = new Histogram("name", Array(0, 2)) 92 | assert(h.name === "name") 93 | assert(h.totalSum === 0) 94 | assert(h.count === 0) 95 | } 96 | test("Histogram correct average after a few observations") { 97 | val g = new Histogram("name", Array(0, 2, 4, 6)) 98 | g.observe(1) 99 | g.observe(3) 100 | g.observe(3) 101 | g.observe(5) 102 | assert(g.totalSum === 12) 103 | assert(g.average === 3) 104 | assert(g.count === 4) 105 | } 106 | 107 | // TODO: test interesting properties of histogram. 108 | } 109 | -------------------------------------------------------------------------------- /desktop-awt/src/main/scala/sgl/awt/AWTWindowProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package awt 3 | 4 | import javax.swing.JFrame 5 | import javax.swing.WindowConstants.EXIT_ON_CLOSE 6 | import javax.swing.JPanel 7 | 8 | import java.awt.event._ 9 | import java.awt.Dimension 10 | import java.awt.Toolkit 11 | import java.awt 12 | 13 | trait AWTWindowProvider extends WindowProvider { 14 | this: GameStateComponent => 15 | 16 | /** The title of the frame */ 17 | val frameTitle: String = "Default App" 18 | 19 | /** The dimension of the game window 20 | * 21 | * This is the exact dimension of the rendering canvas area. 22 | * The full frame will contain a header with some cross button 23 | * and its size will depend on the system (linux mac windows), and 24 | * so it will be slighlty higher than the dimension specified here 25 | * and will vary from system to system. But the playable area is going 26 | * to have a consistent size. 27 | */ 28 | val frameDimension: (Int, Int) 29 | 30 | class ApplicationFrame(canvas: awt.Canvas) extends JFrame { 31 | 32 | this.setTitle(frameTitle) 33 | 34 | // TODO: borderless, but no exit button. 35 | // this.setUndecorated(true) 36 | 37 | val (w, h) = frameDimension 38 | this.getContentPane().setPreferredSize(new Dimension(w, h)) 39 | canvas.setSize(w, h) 40 | 41 | canvas.setFocusable(true) 42 | 43 | this.add(canvas, 0) 44 | this.pack() 45 | 46 | this.setDefaultCloseOperation(EXIT_ON_CLOSE) 47 | 48 | this.setVisible(true) 49 | 50 | this.setResizable(false) 51 | this.setLocationRelativeTo(null) 52 | 53 | } 54 | 55 | /* 56 | * We don't initialize as part of the cake mixin, because 57 | * of the usual issues with initialization order and null pointers 58 | * due to override (frameDimension). They are initialized in main 59 | * instead 60 | */ 61 | var gameCanvas: awt.Canvas = null 62 | var applicationFrame: ApplicationFrame = null 63 | 64 | class AWTWindow extends AbstractWindow { 65 | 66 | override def width: Int = gameCanvas.getWidth 67 | override def height: Int = gameCanvas.getHeight 68 | 69 | /* 70 | * TODO: After doing some research, and trial and errors, it seems 71 | * like getting the screen ppi in Java is not very well supported. As 72 | * a temporary workaround, I export a settings to override the JVM 73 | * dpi with a constant chosen at compile time. This is motly helpful 74 | * for development in the local machine, to play around with different 75 | * PPI and also to make the game looks nice in case the JVM ppi is totally 76 | * out of whack with reality (as I've witnessed with a value of 95 provided 77 | * by the JVM while my actual PPI is about 200, which makes the game 78 | * unplayable). 79 | */ 80 | override def xppi: Float = ScreenForcePPI.getOrElse(Toolkit.getDefaultToolkit().getScreenResolution().toFloat) 81 | override def yppi: Float = ScreenForcePPI.getOrElse(Toolkit.getDefaultToolkit().getScreenResolution().toFloat) 82 | 83 | override def logicalPpi: Float = ScreenForcePPI.getOrElse(Toolkit.getDefaultToolkit().getScreenResolution().toFloat) 84 | } 85 | type Window = AWTWindow 86 | override val Window = new AWTWindow 87 | 88 | /** Override this if you want to force an arbitrary PPI. Typically it's useful for testing how your game will adapt 89 | * to multiple screen densities, instead of testing on multiple platforms. */ 90 | val ScreenForcePPI: Option[Float] = None 91 | } 92 | -------------------------------------------------------------------------------- /desktop-awt/src/main/scala/sgl/awt/FileSave.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package awt 3 | 4 | import scala.io.Source 5 | 6 | /** Implement Save with a standard file */ 7 | class FileSave(filename: String) extends AbstractSave { 8 | 9 | //TODO: should make this a generic putX[X], and it should take 10 | // some implicit param to get the type and have the type info being 11 | // part of the serialization, to make sure we don't parse back Int as String 12 | 13 | // TODO: this is not very safe as it uses ':' as a separator and it wouldn't handle such characters in name or value. 14 | // It also doesn't work with empty strings. 15 | override def putString(name: String, value: String): Unit = { 16 | val rawLines: List[String] = try { 17 | Source.fromFile(filename).getLines().toList 18 | } catch { 19 | case (_: Exception) => List() 20 | } 21 | val parsedLines: List[(String, String)] = rawLines.flatMap(line => try { 22 | val Array(n, v) = line.split(":") 23 | Some(n -> v) 24 | } catch { 25 | case (_: Exception) => None 26 | }) 27 | 28 | val newLines: List[(String, String)] = 29 | if(parsedLines.exists(_._1 == name)) 30 | parsedLines.map{ case (n, v) => 31 | if(n == name) 32 | (n, value.toString) 33 | else 34 | (n, v) 35 | } 36 | else 37 | (name, value.toString) :: parsedLines 38 | 39 | val out = new java.io.PrintWriter(filename, "UTF-8") 40 | try { 41 | out.print(newLines.map(p => p._1 + ":" + p._2).mkString("\n")) 42 | } finally { 43 | out.close 44 | } 45 | } 46 | override def getString(name: String): Option[String] = { 47 | try { 48 | Source.fromFile(filename).getLines().toList.flatMap(line => try { 49 | val Array(id, value) = line.split(":") 50 | if(id == name) Some(value) else None 51 | } catch { 52 | case (_: Exception) => None 53 | }).headOption 54 | } catch { 55 | case (_: Exception) => None 56 | } 57 | } 58 | 59 | override def putInt(name: String, value: Int): Unit = { 60 | putString(name, value.toString) 61 | } 62 | 63 | override def getInt(name: String): Option[Int] = { 64 | getString(name).flatMap(v => try { 65 | Some(v.toInt) 66 | } catch { 67 | case (_: Exception) => None 68 | }) 69 | } 70 | 71 | override def putLong(name: String, value: Long): Unit = { 72 | putString(name, value.toString) 73 | } 74 | 75 | override def getLong(name: String): Option[Long] = { 76 | getString(name).flatMap(v => try { 77 | Some(v.toLong) 78 | } catch { 79 | case (_: Exception) => None 80 | }) 81 | } 82 | 83 | override def putBoolean(name: String, value: Boolean): Unit = { 84 | putString(name, value.toString) 85 | } 86 | override def getBoolean(name: String): Option[Boolean] = { 87 | getString(name).flatMap(v => try { 88 | Some(v.toBoolean) 89 | } catch { 90 | case (_: Exception) => None 91 | }) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /desktop-awt/src/main/scala/sgl/awt/util/LiftJsonProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package awt 3 | package util 4 | 5 | import sgl.util.JsonProvider 6 | 7 | import scala.language.implicitConversions 8 | 9 | import net.liftweb.{json => liftJson} 10 | 11 | trait LiftJsonProvider extends JsonProvider { 12 | 13 | object LiftJson extends Json { 14 | 15 | type JValue = liftJson.JValue 16 | override def parse(raw: String): JValue = { 17 | try { 18 | val v = liftJson.parse(raw) 19 | if(v == liftJson.JNothing) 20 | throw new ParseException("JSON data was incomplete") 21 | /* 22 | * TODO: Find a more efficient solution. 23 | * 24 | * Unfortunately, in order to support the == on 25 | * any JValue, we need to proactively replace all JInt 26 | * from the expression with the corresponding JDouble 27 | * and the approximated value. This is quite inneficient 28 | * but otherwise printing or using == operations will 29 | * behave in an unconsistent manner on the lift-based 30 | * implementation compared to the defined abstract API. 31 | * The root of the problem is discussed in the sgl.util.JsonProvider 32 | * core API class. 33 | */ 34 | v.map{ 35 | case liftJson.JInt(n) => liftJson.JDouble(n.toDouble) 36 | case x => x 37 | } 38 | } catch { 39 | case (e: liftJson.JsonParser.ParseException) => throw new ParseException(e.getMessage) 40 | } 41 | } 42 | 43 | class LiftRichJsonAst(v: liftJson.JValue) extends RichJsonAst { 44 | override def \ (field: String): JValue = v \ field 45 | } 46 | override implicit def richJsonAst(ast: JValue) = new LiftRichJsonAst(ast) 47 | 48 | type JNothing = liftJson.JNothing.type 49 | override val JNothing = liftJson.JNothing 50 | type JNull = liftJson.JNull.type 51 | override val JNull = liftJson.JNull 52 | 53 | object LiftJStringCompanion extends JStringCompanion { 54 | override def unapply(v: JValue): Option[String] = v match { 55 | case (x: liftJson.JString) => liftJson.JString.unapply(x) 56 | case _ => None 57 | } 58 | } 59 | type JString = liftJson.JString 60 | override val JString = LiftJStringCompanion 61 | 62 | object LiftJNumberCompanion extends JNumberCompanion { 63 | override def unapply(v: JValue): Option[Double] = v match { 64 | case liftJson.JDouble(x) => Some(x) 65 | case liftJson.JInt(n) => Some(n.toDouble) 66 | case _ => None 67 | } 68 | } 69 | type JNumber = liftJson.JDouble 70 | override val JNumber = LiftJNumberCompanion 71 | 72 | object LiftJBooleanCompanion extends JBooleanCompanion { 73 | override def unapply(v: JValue): Option[Boolean] = v match { 74 | case (x: liftJson.JBool) => liftJson.JBool.unapply(x) 75 | case _ => None 76 | } 77 | } 78 | type JBoolean = liftJson.JBool 79 | override val JBoolean = LiftJBooleanCompanion 80 | 81 | object LiftJObjectCompanion extends JObjectCompanion { 82 | override def unapply(v: JValue): Option[List[JField]] = v match { 83 | case (x: liftJson.JObject) => liftJson.JObject.unapply(x).map(res => res.map(f => (f.name, f.value))) 84 | case _ => None 85 | } 86 | } 87 | type JObject = liftJson.JObject 88 | override val JObject = LiftJObjectCompanion 89 | 90 | object LiftJArrayCompanion extends JArrayCompanion { 91 | override def unapply(v: JValue): Option[List[JValue]] = v match { 92 | case (x: liftJson.JArray) => liftJson.JArray.unapply(x) 93 | case _ => None 94 | } 95 | } 96 | type JArray = liftJson.JArray 97 | override val JArray = LiftJArrayCompanion 98 | } 99 | 100 | override val Json = LiftJson 101 | 102 | } 103 | -------------------------------------------------------------------------------- /desktop-awt/src/main/scala/sgl/awt/util/TerminalLoggingProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package awt 3 | package util 4 | 5 | import sgl.util._ 6 | 7 | /* 8 | * These are default loggers that make sense in the JVM/Desktop 9 | * environment. They are not necessarly only for AWT, but until 10 | * we get more platforms they should at least be independant from 11 | * the core library since they assume the existence of a terminal. 12 | * They also use some color code to display nice color messages. 13 | */ 14 | 15 | trait TerminalLoggingProvider extends LoggingProvider { 16 | import Logger._ 17 | 18 | private val ErrorPrefix = "[ Error ] " 19 | private val WarningPrefix = "[Warning] " 20 | private val InfoPrefix = "[ Info ] " 21 | private val DebugPrefix = "[ Debug ] " 22 | private val TracePrefix = "[ Trace ] " 23 | 24 | abstract class TerminalLogger extends Logger { 25 | def output(msg: String): Unit 26 | 27 | protected override def log(level: LogLevel, tag: Tag, msg: String): Unit = level match { 28 | case NoLogging => () 29 | case Error => output(reline(ErrorPrefix, tag, msg)) 30 | case Warning => output(reline(WarningPrefix, tag, msg)) 31 | case Info => output(reline(InfoPrefix, tag, msg)) 32 | case Debug => output(reline(DebugPrefix, tag, msg)) 33 | case Trace => output(reline(TracePrefix, tag, msg)) 34 | } 35 | 36 | private def reline(prefix: String, tag: Tag, msg: String): String = { 37 | val colorPrefix = 38 | if(prefix == ErrorPrefix) 39 | Console.RED 40 | else if(prefix == WarningPrefix) 41 | Console.YELLOW 42 | else if(prefix == DebugPrefix) 43 | Console.MAGENTA 44 | else if(prefix == TracePrefix) 45 | Console.GREEN 46 | else //for INFO 47 | Console.BLUE 48 | val colorTag = Console.CYAN 49 | "[" + colorPrefix + prefix.substring(1, prefix.length-2) + Console.RESET + "] " + 50 | "[ " + colorTag + tag.name + Console.RESET + " ] " + 51 | msg.trim.replaceAll("\n", "\n" + (" " * (prefix.size))) 52 | } 53 | } 54 | 55 | } 56 | trait StdErrLoggingProvider extends TerminalLoggingProvider { 57 | abstract class StdErrLogger extends TerminalLogger { 58 | override def output(msg: String): Unit = { 59 | Console.err.println(msg) 60 | } 61 | } 62 | } 63 | 64 | trait DefaultStdErrLoggingProvider extends StdErrLoggingProvider { 65 | val logger = DefaultStdErrLogger 66 | object DefaultStdErrLogger extends StdErrLogger { 67 | override val logLevel: Logger.LogLevel = Logger.Warning 68 | } 69 | } 70 | 71 | trait VerboseStdErrLoggingProvider extends StdErrLoggingProvider { 72 | val logger = VerboseStdErrLogger 73 | object VerboseStdErrLogger extends StdErrLogger { 74 | import Logger._ 75 | override val logLevel: LogLevel = Debug 76 | } 77 | } 78 | 79 | trait TraceStdErrLoggingProvider extends StdErrLoggingProvider { 80 | val logger = TraceStdErrLogger 81 | object TraceStdErrLogger extends StdErrLogger { 82 | import Logger._ 83 | override val logLevel: LogLevel = Trace 84 | } 85 | } 86 | 87 | 88 | 89 | trait StdOutLoggingProvider extends TerminalLoggingProvider { 90 | abstract class StdOutLogger extends TerminalLogger { 91 | override def output(msg: String): Unit = { 92 | Console.out.println(msg) 93 | } 94 | } 95 | } 96 | 97 | trait DefaultStdOutLoggingProvider extends StdOutLoggingProvider { 98 | val logger = DefaultStdOutLogger 99 | object DefaultStdOutLogger extends StdOutLogger { 100 | override val logLevel: Logger.LogLevel = Logger.Warning 101 | } 102 | } 103 | 104 | trait VerboseStdOutLoggingProvider extends StdOutLoggingProvider { 105 | val logger = VerboseStdOutLogger 106 | object VerboseStdOutLogger extends StdOutLogger { 107 | override val logLevel: Logger.LogLevel = Logger.Debug 108 | } 109 | } 110 | 111 | trait TraceStdOutLoggingProvider extends StdOutLoggingProvider { 112 | val logger = TraceStdOutLogger 113 | object TraceStdOutLogger extends StdOutLogger { 114 | override val logLevel: Logger.LogLevel = Logger.Trace 115 | } 116 | } 117 | 118 | // vim: set ts=4 sw=4 et: 119 | -------------------------------------------------------------------------------- /desktop-awt/src/test/scala/sgl/awt/util/LiftJsonProviderSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.awt.util 2 | 3 | import sgl.util.JsonProviderAbstractSuite 4 | 5 | class LiftJsonProviderSuite extends JsonProviderAbstractSuite with LiftJsonProvider 6 | -------------------------------------------------------------------------------- /desktop-native/src/main/scala/sgl/native/NativeAudioProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package native 3 | 4 | import sgl.util.Loader 5 | 6 | trait NativeAudioProvider extends AudioProvider { 7 | this: NativeSystemProvider => 8 | 9 | object NativeAudio extends Audio { 10 | /** Not supported. */ 11 | class Sound extends AbstractSound { 12 | 13 | type PlayedSound = Int 14 | 15 | override def play(volume: Float): Option[PlayedSound] = None 16 | override def withConfig(loop: Int, rate: Float): Sound = this 17 | override def dispose(): Unit = {} 18 | 19 | override def stop(id: PlayedSound): Unit = {} 20 | override def pause(id: PlayedSound): Unit = {} 21 | override def resume(id: PlayedSound): Unit = {} 22 | override def endLoop(id: PlayedSound): Unit = {} 23 | } 24 | /** Not supported. */ 25 | override def loadSound(path: ResourcePath, extras: ResourcePath*): Loader[Sound] = Loader.successful(new Sound) 26 | 27 | /** Not supported. */ 28 | class Music extends AbstractMusic { 29 | override def play(): Unit = {} 30 | override def pause(): Unit = {} 31 | override def stop(): Unit = {} 32 | override def setVolume(volume: Float): Unit = {} 33 | override def setLooping(isLooping: Boolean): Unit = {} 34 | override def dispose(): Unit = {} 35 | } 36 | /** Not supported. */ 37 | override def loadMusic(path: ResourcePath, extras: ResourcePath*): Loader[Music] = Loader.successful(new Music) 38 | } 39 | override val Audio = NativeAudio 40 | 41 | } 42 | -------------------------------------------------------------------------------- /desktop-native/src/main/scala/sgl/native/NativeInputProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package native 3 | 4 | import sgl.util._ 5 | 6 | import scalanative.unsafe._ 7 | import scalanative.unsigned._ 8 | 9 | import sdl2.SDL._ 10 | import sdl2.Extras._ 11 | 12 | trait NativeInputProvider { 13 | this: NativeWindowProvider with NativeGraphicsProvider with LoggingProvider => 14 | 15 | private implicit val LogTag = Logger.Tag("sgl.native.input") 16 | 17 | def registerInputListeners(): Unit = { } 18 | 19 | // Events are being processed in the game loop main thread, so they can be dispatched right away. 20 | def handleEvent(event: Ptr[SDL_Event]): Unit = { 21 | event.type_ match { 22 | case SDL_KEYDOWN => 23 | val keyEvent = event.key 24 | if(keyEvent.repeat == 0.toUByte) //SDL2 re-trigger events when holding the key for some time 25 | keycodeToEvent 26 | .andThen(key => 27 | Input.inputProcessor.keyDown(key)) 28 | .applyOrElse(keyEvent.keysym.sym, 29 | (keycode: SDL_Keycode) => logger.debug("ignoring event with keycode: " + keycode)) 30 | case SDL_KEYUP => 31 | keycodeToEvent 32 | .andThen(key => 33 | Input.inputProcessor.keyUp(key)) 34 | .applyOrElse(event.key.keysym.sym, 35 | (keycode: SDL_Keycode) => logger.debug("ignoring event with keycode: " + keycode)) 36 | 37 | case SDL_MOUSEBUTTONDOWN => 38 | //TODO: check 'which' field to ignore TOUCH events 39 | val mouseButtonEvent = event.button 40 | buttonToMouseButton(mouseButtonEvent.button).foreach(mb => 41 | Input.inputProcessor.mouseDown(mouseButtonEvent.x, mouseButtonEvent.y, mb) 42 | ) 43 | 44 | case SDL_MOUSEBUTTONUP => 45 | //TODO: check 'which' field to ignore TOUCH events 46 | val mouseButtonEvent = event.button 47 | buttonToMouseButton(mouseButtonEvent.button).foreach(mb => 48 | Input.inputProcessor.mouseUp(mouseButtonEvent.x, mouseButtonEvent.y, mb) 49 | ) 50 | 51 | case SDL_MOUSEMOTION => 52 | val motionEvent = event.motion 53 | Input.inputProcessor.mouseMoved(motionEvent.x, motionEvent.y) 54 | 55 | case _ => 56 | () 57 | } 58 | } 59 | 60 | private def buttonToMouseButton(mouseButton: UByte): Option[Input.MouseButtons.MouseButton] = 61 | mouseButton match { 62 | case SDL_BUTTON_LEFT => Some(Input.MouseButtons.Left) 63 | case SDL_BUTTON_MIDDLE => Some(Input.MouseButtons.Middle) 64 | case SDL_BUTTON_RIGHT => Some(Input.MouseButtons.Right) 65 | } 66 | 67 | private def keycodeToEvent: PartialFunction[SDL_Keycode, Input.Keys.Key] = { 68 | case SDLK_a => Input.Keys.A 69 | case SDLK_b => Input.Keys.B 70 | case SDLK_c => Input.Keys.C 71 | case SDLK_d => Input.Keys.D 72 | case SDLK_e => Input.Keys.E 73 | case SDLK_f => Input.Keys.F 74 | case SDLK_g => Input.Keys.G 75 | case SDLK_h => Input.Keys.H 76 | case SDLK_i => Input.Keys.I 77 | case SDLK_j => Input.Keys.J 78 | case SDLK_k => Input.Keys.K 79 | case SDLK_l => Input.Keys.L 80 | case SDLK_m => Input.Keys.M 81 | case SDLK_n => Input.Keys.N 82 | case SDLK_o => Input.Keys.O 83 | case SDLK_p => Input.Keys.P 84 | case SDLK_q => Input.Keys.Q 85 | case SDLK_r => Input.Keys.R 86 | case SDLK_s => Input.Keys.S 87 | case SDLK_t => Input.Keys.T 88 | case SDLK_u => Input.Keys.U 89 | case SDLK_v => Input.Keys.V 90 | case SDLK_w => Input.Keys.W 91 | case SDLK_x => Input.Keys.X 92 | case SDLK_y => Input.Keys.Y 93 | case SDLK_z => Input.Keys.Z 94 | 95 | case SDLK_0 => Input.Keys.Num0 96 | case SDLK_1 => Input.Keys.Num1 97 | case SDLK_2 => Input.Keys.Num2 98 | case SDLK_3 => Input.Keys.Num3 99 | case SDLK_4 => Input.Keys.Num4 100 | case SDLK_5 => Input.Keys.Num5 101 | case SDLK_6 => Input.Keys.Num6 102 | case SDLK_7 => Input.Keys.Num7 103 | case SDLK_8 => Input.Keys.Num8 104 | case SDLK_9 => Input.Keys.Num9 105 | 106 | case SDLK_KP_0 => Input.Keys.Num0 107 | case SDLK_KP_1 => Input.Keys.Num1 108 | case SDLK_KP_2 => Input.Keys.Num2 109 | case SDLK_KP_3 => Input.Keys.Num3 110 | case SDLK_KP_4 => Input.Keys.Num4 111 | case SDLK_KP_5 => Input.Keys.Num5 112 | case SDLK_KP_6 => Input.Keys.Num6 113 | case SDLK_KP_7 => Input.Keys.Num7 114 | case SDLK_KP_8 => Input.Keys.Num8 115 | case SDLK_KP_9 => Input.Keys.Num9 116 | 117 | case SDLK_SPACE => Input.Keys.Space 118 | 119 | case SDLK_UP => Input.Keys.Up 120 | case SDLK_DOWN => Input.Keys.Down 121 | case SDLK_LEFT => Input.Keys.Left 122 | case SDLK_RIGHT => Input.Keys.Right 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /desktop-native/src/main/scala/sgl/native/NativeSystemProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package native 3 | 4 | import sgl.util._ 5 | 6 | import java.net.URI 7 | import java.awt.Desktop 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | import scala.language.implicitConversions 12 | 13 | trait NativeSystemProvider extends SystemProvider with PartsResourcePathProvider { 14 | 15 | object NativeSystem extends System { 16 | 17 | override def exit(): Unit = { 18 | sys.exit() 19 | } 20 | 21 | override def currentTimeMillis: Long = java.lang.System.currentTimeMillis 22 | override def nanoTime: Long = java.lang.System.nanoTime 23 | 24 | override def loadText(path: ResourcePath): Loader[Array[String]] = { 25 | ??? 26 | //val is = getClass.getClassLoader.getResourceAsStream(path) 27 | //scala.io.Source.fromInputStream(is).getLines 28 | } 29 | 30 | override def loadBinary(path: ResourcePath): Loader[Array[Byte]] = { 31 | ??? 32 | } 33 | 34 | override def openWebpage(uri: URI): Unit = { 35 | ??? 36 | } 37 | } 38 | 39 | override val System = NativeSystem 40 | 41 | // TODO: This is not really a root as we start with a first part ("assets"). We 42 | // should instead add the assets prefix at the time when we convert the parts to 43 | // a path. For now, it's a fine hack to get something working though. 44 | override val ResourcesRoot: ResourcePath = PartsResourcePath(Vector("assets")) 45 | // TODO: Add support for multi dpi in loadImage (so do not always use drawable-mdpi). 46 | override val MultiDPIResourcesRoot: ResourcePath = PartsResourcePath(Vector("assets", "drawable-mdpi")) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /desktop-native/src/main/scala/sgl/native/NativeWindowProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package native 3 | 4 | import scalanative.unsafe._ 5 | 6 | import sdl2.SDL._ 7 | import sdl2.Extras._ 8 | 9 | trait NativeWindowProvider extends WindowProvider { 10 | this: GameStateComponent with NativeGraphicsProvider => 11 | 12 | val frameDimension: (Int, Int) 13 | 14 | class NativeWindow extends AbstractWindow { 15 | override def width: Int = frameDimension._1 16 | override def height: Int = frameDimension._2 17 | 18 | // TODO: should refresh when Window is resized or dpis changes. 19 | private var _xppi: Float = 0f 20 | private var _yppi: Float = 0f 21 | private var _ppi: Float = 0f 22 | private def computePPIs(): Unit = { 23 | val ddpi: Ptr[CFloat] = stackalloc[CFloat] 24 | val hdpi: Ptr[CFloat] = stackalloc[CFloat] 25 | val vdpi: Ptr[CFloat] = stackalloc[CFloat] 26 | SDL_GetDisplayDPI(0, ddpi, hdpi, vdpi) 27 | _xppi = !hdpi 28 | _yppi = !vdpi 29 | _ppi = !ddpi 30 | } 31 | 32 | override def xppi: Float = if(_xppi != 0f) _xppi else { 33 | computePPIs() 34 | _xppi 35 | } 36 | override def yppi: Float = if(_yppi != 0f) _yppi else { 37 | computePPIs() 38 | _yppi 39 | } 40 | 41 | // TODO: rounding? 42 | override def logicalPpi: Float = if(_ppi != 0f) _ppi else { 43 | computePPIs() 44 | _ppi 45 | } 46 | } 47 | type Window = NativeWindow 48 | override val Window = new NativeWindow 49 | 50 | ///** The name of the window */ 51 | //val windowTitle: String 52 | 53 | //TODO: provide a WindowDimension object, with either fixed width/height or FullScreen 54 | //abstract class WindowDimension 55 | //case class FixedWindowDimension(width: Int, height: Int) 56 | //case object FullScreen 57 | //case class ResizableWIndowDimension(width: Int, height: Int) 58 | //val WindowDimension: WindowDimension 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /desktop-native/src/main/scala/sgl/native/util/TerminalLoggingProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package native 3 | package util 4 | 5 | import sgl.util._ 6 | 7 | /* 8 | * These are default loggers that make sense in the JVM/Desktop 9 | * environment. They are not necessarly only for AWT, but until 10 | * we get more platforms they should at least be independant from 11 | * the core library since they assume the existence of a terminal. 12 | * They also use some color code to display nice color messages. 13 | */ 14 | 15 | trait TerminalLoggingProvider extends LoggingProvider { 16 | import Logger._ 17 | 18 | private val ErrorPrefix = "[ Error ] " 19 | private val WarningPrefix = "[Warning] " 20 | private val InfoPrefix = "[ Info ] " 21 | private val DebugPrefix = "[ Debug ] " 22 | private val TracePrefix = "[ Trace ] " 23 | 24 | abstract class TerminalLogger extends Logger { 25 | def output(msg: String): Unit 26 | 27 | protected override def log(level: LogLevel, tag: Tag, msg: String): Unit = level match { 28 | case NoLogging => () 29 | case Error => output(reline(ErrorPrefix, tag, msg)) 30 | case Warning => output(reline(WarningPrefix, tag, msg)) 31 | case Info => output(reline(InfoPrefix, tag, msg)) 32 | case Debug => output(reline(DebugPrefix, tag, msg)) 33 | case Trace => output(reline(TracePrefix, tag, msg)) 34 | } 35 | 36 | private def reline(prefix: String, tag: Tag, msg: String): String = { 37 | val colorPrefix = 38 | if(prefix == ErrorPrefix) 39 | Console.RED 40 | else if(prefix == WarningPrefix) 41 | Console.YELLOW 42 | else if(prefix == DebugPrefix) 43 | Console.MAGENTA 44 | else if(prefix == TracePrefix) 45 | Console.GREEN 46 | else //for INFO 47 | Console.BLUE 48 | val colorTag = Console.CYAN 49 | "[" + colorPrefix + prefix.substring(1, prefix.length-2) + Console.RESET + "] " + 50 | "[ " + colorTag + tag.name + Console.RESET + " ] " + 51 | msg//.trim.replaceAll("\n", "\n" + (" " * (prefix.size))) 52 | } 53 | } 54 | 55 | } 56 | trait StdErrLoggingProvider extends TerminalLoggingProvider { 57 | abstract class StdErrLogger extends TerminalLogger { 58 | override def output(msg: String): Unit = { 59 | Console.err.println(msg) 60 | } 61 | } 62 | } 63 | 64 | trait DefaultStdErrLoggingProvider extends StdErrLoggingProvider { 65 | val logger = DefaultStdErrLogger 66 | object DefaultStdErrLogger extends StdErrLogger { 67 | override val logLevel: Logger.LogLevel = Logger.Warning 68 | } 69 | } 70 | 71 | trait VerboseStdErrLoggingProvider extends StdErrLoggingProvider { 72 | val logger = VerboseStdErrLogger 73 | object VerboseStdErrLogger extends StdErrLogger { 74 | import Logger._ 75 | override val logLevel: LogLevel = Debug 76 | } 77 | } 78 | 79 | trait TraceStdErrLoggingProvider extends StdErrLoggingProvider { 80 | val logger = TraceStdErrLogger 81 | object TraceStdErrLogger extends StdErrLogger { 82 | import Logger._ 83 | override val logLevel: LogLevel = Trace 84 | } 85 | } 86 | 87 | 88 | 89 | trait StdOutLoggingProvider extends TerminalLoggingProvider { 90 | abstract class StdOutLogger extends TerminalLogger { 91 | override def output(msg: String): Unit = { 92 | Console.out.println(msg) 93 | } 94 | } 95 | } 96 | 97 | trait DefaultStdOutLoggingProvider extends StdOutLoggingProvider { 98 | val logger = DefaultStdOutLogger 99 | object DefaultStdOutLogger extends StdOutLogger { 100 | override val logLevel: Logger.LogLevel = Logger.Warning 101 | } 102 | } 103 | 104 | trait VerboseStdOutLoggingProvider extends StdOutLoggingProvider { 105 | val logger = VerboseStdOutLogger 106 | object VerboseStdOutLogger extends StdOutLogger { 107 | override val logLevel: Logger.LogLevel = Logger.Debug 108 | } 109 | } 110 | 111 | trait TraceStdOutLoggingProvider extends StdOutLoggingProvider { 112 | val logger = TraceStdOutLogger 113 | object TraceStdOutLogger extends StdOutLogger { 114 | override val logLevel: Logger.LogLevel = Logger.Trace 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/.DS_Store -------------------------------------------------------------------------------- /examples/board/README.md: -------------------------------------------------------------------------------- 1 | # Board Example 2 | 3 | Simple chess-like pattern for a board, using viewport for mapping 4 | coordinates. 5 | -------------------------------------------------------------------------------- /examples/board/core/src/main/scala/App.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.board 2 | package core 3 | 4 | import sgl._ 5 | import sgl.util._ 6 | import sgl.scene._ 7 | import sgl.scene.ui._ 8 | 9 | trait AbstractApp extends ScreensComponent { 10 | this: GameApp with ViewportComponent => 11 | 12 | override def startingScreen: GameScreen = new BoardScreen 13 | 14 | } 15 | -------------------------------------------------------------------------------- /examples/board/core/src/main/scala/MainScreen.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.board 2 | package core 3 | 4 | import sgl._ 5 | import geometry._ 6 | import scene._ 7 | import scene.ui._ 8 | import util._ 9 | 10 | trait ScreensComponent { 11 | this: GraphicsProvider with SystemProvider with WindowProvider 12 | with GameStateComponent with LoggingProvider with ViewportComponent => 13 | 14 | private implicit val LogTag = Logger.Tag("main-screen") 15 | 16 | class BoardScreen extends GameScreen with InputProcessor { 17 | 18 | override def name: String = "board-screen" 19 | 20 | val BoardSize: Int = 1 21 | val WorldHeight: Int = 10 22 | val WorldWidth: Float = Window.width*(WorldHeight/Window.height.toFloat) 23 | 24 | val viewport = new Viewport(Window.width, Window.height) 25 | viewport.setCamera(0, 0, WorldWidth.toFloat, WorldHeight.toFloat) 26 | viewport.scalingStrategy = Viewport.Fit 27 | 28 | val p = (0f, 0f) 29 | 30 | override def keyDown(e: Input.Keys.Key): Boolean = { 31 | e match { 32 | case Input.Keys.Down => 33 | viewport.translateCamera(0, 0.5f) 34 | case Input.Keys.Up => 35 | viewport.translateCamera(0, -0.5f) 36 | case Input.Keys.Left => 37 | viewport.translateCamera(-0.5f, 0) 38 | case Input.Keys.Right => 39 | viewport.translateCamera(0.5f, 0) 40 | case _ => 41 | } 42 | true 43 | } 44 | 45 | Input.setInputProcessor(this) 46 | 47 | override def update(dt: Long): Unit = { } 48 | 49 | override def render(canvas: Graphics.Canvas): Unit = { 50 | canvas.drawRect(0, 0, Window.width, Window.height, Graphics.defaultPaint.withColor(Graphics.Color.Blue)) 51 | viewport.withViewport(canvas) { 52 | for(i <- 0 until 100) { 53 | for(j <- 0 until 100) { 54 | val color = if((i+j) % 2 == 0) Graphics.Color.White else Graphics.Color.Black 55 | canvas.drawRect(j.toFloat, i.toFloat, 1f, 1f, Graphics.defaultPaint.withColor(color)) 56 | } 57 | } 58 | canvas.drawCircle(p._1 + 0.5f, p._2 + 0.5f, 0.5f, Graphics.defaultPaint.withColor(Graphics.Color.Green)) 59 | } 60 | } 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /examples/board/desktop-awt/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.board 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.{GameLoopStatisticsComponent, ViewportComponent} 7 | import sgl.awt._ 8 | import sgl.awt.util._ 9 | 10 | /** Wire backend to the App here */ 11 | object Main extends AbstractApp with AWTApp 12 | with VerboseStdErrLoggingProvider 13 | with ViewportComponent { 14 | 15 | override val TargetFps = Some(60) 16 | 17 | override val frameDimension = (450, 780) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/board/desktop-native/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.board 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.ViewportComponent 7 | import sgl.native._ 8 | import sgl.native.util._ 9 | 10 | /** Wire backend to the App here */ 11 | object Main extends AbstractApp with NativeApp 12 | with TraceStdErrLoggingProvider 13 | with ViewportComponent { 14 | 15 | override val TargetFps = Some(60) 16 | 17 | override val frameDimension = (450, 780) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/board/html5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/board/html5/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.board 2 | package html5 3 | 4 | import sgl.html5._ 5 | import sgl.html5.util._ 6 | import sgl.html5.themes._ 7 | import sgl._ 8 | import sgl.scene._ 9 | import sgl.scene.ui._ 10 | 11 | object Main extends Html5App with core.AbstractApp 12 | with Html5VerboseConsoleLoggingProvider 13 | with ViewportComponent { 14 | 15 | override val TargetFps = None 16 | 17 | override val GameCanvasID: String = "my_canvas" 18 | 19 | override val theme = new DefaultTheme { 20 | override val maxFrame = (400, 600) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /examples/hello/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/.DS_Store -------------------------------------------------------------------------------- /examples/hello/README.md: -------------------------------------------------------------------------------- 1 | Test 2 | ========== 3 | 4 | This is the simplest possible "game", which is cross-platform and 5 | can be used as a starting point to test and play around with the 6 | library. 7 | -------------------------------------------------------------------------------- /examples/hello/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/hello/android/src/main/res/drawable-mdpi/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/android/src/main/res/drawable-mdpi/character.png -------------------------------------------------------------------------------- /examples/hello/android/src/main/scala/MainActivity.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl 2 | package test.android 3 | 4 | import android.app.Activity 5 | import android.os.Bundle 6 | 7 | import sgl.GameLoopStatisticsComponent 8 | import sgl.android._ 9 | import sgl.util._ 10 | 11 | import test.core._ 12 | 13 | class MainActivity extends Activity with AbstractApp with AndroidApp 14 | with NoLoggingProvider with GameLoopStatisticsComponent { 15 | 16 | override val TargetFps = Some(40) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /examples/hello/assets/audio/beep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/assets/audio/beep.wav -------------------------------------------------------------------------------- /examples/hello/assets/audio/music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/assets/audio/music.ogg -------------------------------------------------------------------------------- /examples/hello/assets/audio/music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/assets/audio/music.wav -------------------------------------------------------------------------------- /examples/hello/assets/drawable-mdpi/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/assets/drawable-mdpi/character.png -------------------------------------------------------------------------------- /examples/hello/assets/drawable-xhdpi/character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/hello/assets/drawable-xhdpi/character.png -------------------------------------------------------------------------------- /examples/hello/core/src/main/scala/App.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.test 2 | package core 3 | 4 | import sgl._ 5 | import sgl.proxy._ 6 | import sgl.util._ 7 | 8 | trait AbstractApp extends GameApp with MainScreenComponent { 9 | this: SchedulerProvider => 10 | 11 | override def startingScreen: GameScreen = LoadingScreen 12 | 13 | } 14 | 15 | object Wiring { 16 | 17 | def wire(platformProxy: PlatformProxy): ProxiedGameApp = { 18 | new AbstractApp with ProxyPlatformProvider { 19 | override val PlatformProxy: PlatformProxy = platformProxy 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/hello/desktop-awt/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.test 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.GameLoopStatisticsComponent 7 | import sgl.awt._ 8 | import sgl.awt.util._ 9 | 10 | 11 | /** Wire backend to the App here */ 12 | object Main extends AbstractApp with AWTApp 13 | with VerboseStdErrLoggingProvider { 14 | 15 | override val TargetFps = Some(60) 16 | 17 | override val frameDimension = (800, 800) 18 | 19 | override val ScreenForcePPI = Some(160) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/hello/desktop-native/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.test 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.{GameLoopStatisticsComponent} 7 | import sgl.native._ 8 | import sgl.native.util._ 9 | 10 | 11 | /** Wire backend to the App here */ 12 | object Main extends AbstractApp with NativeApp 13 | with TraceStdErrLoggingProvider { 14 | 15 | override val TargetFps = Some(60) 16 | 17 | override val frameDimension = (800, 600) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/hello/html5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/hello/html5/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.test 2 | package html5 3 | 4 | import sgl.html5._ 5 | import sgl.html5.util._ 6 | import sgl.html5.themes._ 7 | import sgl._ 8 | 9 | object Main extends core.AbstractApp with Html5App 10 | with Html5VerboseConsoleLoggingProvider { 11 | 12 | override val TargetFps = None 13 | 14 | override val GameCanvasID: String = "my_canvas" 15 | 16 | override val theme = new FullScreenTheme 17 | //override val theme = new DefaultTheme { 18 | // override val maxFrame = (600, 600) 19 | // override val optimalFrame = Some((600, 500)) 20 | //} 21 | 22 | } 23 | -------------------------------------------------------------------------------- /examples/hello/html5/static/audio: -------------------------------------------------------------------------------- 1 | ../../assets/audio -------------------------------------------------------------------------------- /examples/hello/html5/static/drawable-mdpi: -------------------------------------------------------------------------------- 1 | ../../assets/drawable-mdpi -------------------------------------------------------------------------------- /examples/hello/html5/static/drawable-xhdpi: -------------------------------------------------------------------------------- 1 | ../../assets/drawable-xhdpi -------------------------------------------------------------------------------- /examples/menu/README.md: -------------------------------------------------------------------------------- 1 | # Menus Examples 2 | 3 | This is a simple "game", which consists only of menus. 4 | It is mostly meant to test the features offered by the sgl.scene 5 | package. 6 | -------------------------------------------------------------------------------- /examples/menu/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/menu/android/src/main/scala/MainActivity.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl 2 | package menu.android 3 | 4 | import android.app.Activity 5 | import android.os.Bundle 6 | 7 | import sgl.{GameLoopStatisticsComponent, ViewportComponent} 8 | import sgl.android._ 9 | import sgl.util._ 10 | import sgl.scene._ 11 | 12 | import test.core._ 13 | 14 | class MainActivity extends Activity with AbstractApp with AndroidApp 15 | with NoLoggingProvider with GameLoopStatisticsComponent 16 | with SceneComponent with ViewportComponent { 17 | 18 | override val TargetFps = Some(40) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /examples/menu/core/src/main/scala/App.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.menu 2 | package core 3 | 4 | import sgl._ 5 | import sgl.util._ 6 | import sgl.scene._ 7 | import sgl.scene.ui._ 8 | 9 | trait AbstractApp extends ScreensComponent { 10 | this: GameApp with ViewportComponent with SceneComponent with PopupsComponent => 11 | 12 | override def startingScreen: GameScreen = new LevelsScreen 13 | 14 | } 15 | -------------------------------------------------------------------------------- /examples/menu/core/src/main/scala/MainScreen.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.menu 2 | package core 3 | 4 | import sgl._ 5 | import geometry._ 6 | import scene._ 7 | import scene.ui._ 8 | import util._ 9 | 10 | trait ScreensComponent { 11 | this: GraphicsProvider with SystemProvider with WindowProvider 12 | with GameStateComponent with LoggingProvider 13 | with ViewportComponent with SceneComponent with PopupsComponent => 14 | 15 | private implicit val LogTag = Logger.Tag("main-screen") 16 | 17 | class LevelsScreen extends GameScreen { 18 | 19 | override def name: String = "LevelsScreen" 20 | 21 | val viewport = new Viewport(Window.width, Window.height) 22 | 23 | val scene = new SceneGraph(Window.width, Window.height, viewport) 24 | 25 | val levelsPane = new ScrollPane(0, 0, Window.width.toFloat, Window.height.toFloat, Window.width.toFloat, 3*Window.height.toFloat) 26 | scene.addNode(levelsPane) 27 | 28 | class LevelButton(i: Int, _x: Float, _y: Float) extends Button(_x, _y, 100, 30) { 29 | override def notifyClick(x: Float, y: Float): Unit = { 30 | println(s"button $i clicked at ($x, $y)") 31 | } 32 | override def renderPressed(canvas: Graphics.Canvas): Unit = { 33 | val color = Graphics.defaultPaint.withColor(Graphics.Color.Red) 34 | canvas.drawRect(x, y, width, height, color) 35 | } 36 | override def renderRegular(canvas: Graphics.Canvas): Unit = { 37 | val color = Graphics.defaultPaint.withColor(Graphics.Color.Green) 38 | canvas.drawRect(x, y, width, height, color) 39 | } 40 | 41 | override def notifyMoved(x: Float, y: Float): Unit = { 42 | println("moved") 43 | } 44 | } 45 | for(i <- 1 to 100) { 46 | val button = new LevelButton(i, 20, (i*50).toFloat) 47 | levelsPane.addNode(button) 48 | } 49 | val dialog = 50 | new DialogPopup( 51 | Window.width.toFloat, Window.height.toFloat, 52 | new Dialog(Window.dp2px(400).toFloat, 53 | "Hey there, do you like the weather ok?", 54 | List(("Yes", () => { println("yes") }), 55 | ("Nope", () => { println("nope") }), 56 | ("Meh", () => { println("meh") })), 57 | Window.dp2px(36), Graphics.Color.White) 58 | ) { 59 | override val backgroundColor = Graphics.Color.rgba(0,0,0,150) 60 | } 61 | scene.addNode(dialog) 62 | 63 | Input.setInputProcessor(new CombinedInputProcessor(scene, new InputProcessor { 64 | override def keyDown(key: Input.Keys.Key): Boolean = { 65 | if(key == Input.Keys.P) 66 | dialog.show() 67 | true 68 | } 69 | })) 70 | 71 | override def update(dt: Long): Unit = { 72 | scene.update(dt) 73 | } 74 | 75 | override def render(canvas: Graphics.Canvas): Unit = { 76 | canvas.drawRect(0, 0, Window.width.toFloat, Window.height.toFloat, Graphics.defaultPaint.withColor(Graphics.Color.Black)) 77 | scene.render(canvas) 78 | } 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /examples/menu/desktop-awt/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.menu 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.{GameLoopStatisticsComponent, ViewportComponent} 7 | import sgl.awt._ 8 | import sgl.awt.util._ 9 | import sgl.scene._ 10 | import sgl.scene.ui._ 11 | 12 | /** Wire backend to the App here */ 13 | object Main extends AbstractApp with AWTApp 14 | with VerboseStdErrLoggingProvider 15 | with SceneComponent with PopupsComponent with ViewportComponent { 16 | 17 | override val TargetFps = Some(60) 18 | 19 | override val frameDimension = (400, 600) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/menu/desktop-native/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.menu 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.{GameLoopStatisticsComponent, ViewportComponent} 7 | import sgl.native._ 8 | import sgl.native.util._ 9 | import sgl.scene._ 10 | 11 | 12 | /** Wire backend to the App here */ 13 | object Main extends AbstractApp with NativeApp 14 | with TraceStdErrLoggingProvider with GameLoopStatisticsComponent 15 | with SceneComponent with ViewportComponent { 16 | 17 | override val TargetFps = Some(60) 18 | 19 | override val frameDimension = (800, 600) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/menu/html5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/menu/html5/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.menu 2 | package html5 3 | 4 | import sgl.html5._ 5 | import sgl.html5.util._ 6 | import sgl.html5.themes._ 7 | import sgl._ 8 | import sgl.scene._ 9 | import sgl.scene.ui._ 10 | 11 | object Main extends Html5App with core.AbstractApp 12 | with Html5VerboseConsoleLoggingProvider 13 | with SceneComponent with PopupsComponent with ViewportComponent { 14 | 15 | override val TargetFps = None 16 | 17 | override val GameCanvasID: String = "my_canvas" 18 | 19 | override val theme = new DefaultTheme { 20 | override val maxFrame = (400, 600) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /examples/platformer/assets/drawable-mdpi/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/platformer/assets/drawable-mdpi/player.png -------------------------------------------------------------------------------- /examples/platformer/assets/drawable-mdpi/rat-trap-feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/platformer/assets/drawable-mdpi/rat-trap-feature.png -------------------------------------------------------------------------------- /examples/platformer/assets/drawable-mdpi/tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regb/scala-game-library/0bf9ca45db552a73ac1abab9c29d9a8000f94552/examples/platformer/assets/drawable-mdpi/tileset.png -------------------------------------------------------------------------------- /examples/platformer/core/src/main/scala/App.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.platformer 2 | package core 3 | 4 | import sgl._ 5 | import sgl.util._ 6 | import sgl.util.metrics.InstrumentationProvider 7 | import sgl.tiled._ 8 | 9 | trait AbstractApp extends MainScreenComponent { 10 | this: GameApp with InstrumentationProvider 11 | with TiledMapRendererComponent with TmxJsonParserComponent with LoggingProvider => 12 | 13 | override def startingScreen: GameScreen = new MainScreen 14 | 15 | } 16 | -------------------------------------------------------------------------------- /examples/platformer/desktop-awt/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.platformer 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.GameLoopStatisticsComponent 7 | import sgl.util.metrics._ 8 | import sgl.awt._ 9 | import sgl.awt.util._ 10 | import sgl.tiled._ 11 | 12 | 13 | /** Wire backend to the App here */ 14 | object Main extends AbstractApp with AWTApp 15 | with VerboseStdErrLoggingProvider 16 | // Comment out this line and uncomment next one if you do not want instrumentation. 17 | with GameLoopStatisticsComponent with DefaultInstrumentationProvider 18 | // with NoInstrumentationProvider 19 | with TiledMapRendererComponent with TmxJsonParserComponent with LiftJsonProvider { 20 | 21 | override val TargetFps = Some(60) 22 | 23 | override val frameDimension = (800, 800) 24 | 25 | // TODO: The dpi auto-scaling of AWT that is done for -mdpi assets doesn't work with Tiled. Tiled 26 | // files are all hard-coded to the exact pixel dimensions of the tileset, and thus we need to do 27 | // some mapping at some point in the rendering of tiled, which is currently not implemented. This 28 | // is because the tileset is auto-scaled when loaded, and then it's not aligned with the values in the json file. 29 | // Alternative is to use a file that is not dpi-dependent, and instead is just loaded as is. That could 30 | // be an alternative loading function. 31 | // TODO: which one is better? Should we even support multi-dpi? It makes things a lot more complicated, but it just seems 32 | // to make sense to have it for optimizing memory usage depending on the platform. 33 | // This settings works because this pretends like the screen is exactly mdpi (160 ppi) and thus no need to scale at all. 34 | override val ScreenForcePPI = Some(160) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/platformer/desktop-native/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.platformer 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.GameLoopStatisticsComponent 7 | import sgl.util._ 8 | import sgl.util.metrics._ 9 | import sgl.native._ 10 | import sgl.native.util._ 11 | import sgl.tiled._ 12 | 13 | /** Wire backend to the App here */ 14 | object Main extends AbstractApp with NativeApp 15 | with VerboseStdErrLoggingProvider 16 | with GameLoopStatisticsComponent with DefaultInstrumentationProvider 17 | with TiledMapRendererComponent with TmxJsonParserComponent with JsonProvider { 18 | 19 | override val TargetFps = Some(60) 20 | 21 | override val frameDimension = (600, 800) 22 | 23 | override val Json: Json = ??? 24 | 25 | } 26 | -------------------------------------------------------------------------------- /examples/reactive-bird/README.md: -------------------------------------------------------------------------------- 1 | Reactive Bird 2 | ============= 3 | 4 | Reactive Bird is an open-source clone to Flappy Bird. 5 | 6 | Reactive Bird aims at teaching people how to use the 7 | [SGL](https://github.com/regb/scala-game-library/), in particular 8 | its (future) reactive module. 9 | -------------------------------------------------------------------------------- /examples/snake/README.md: -------------------------------------------------------------------------------- 1 | Snake 2 | ===== 3 | 4 | A very simple snake game based on the original implementation demo by Denys 5 | Shabalin at [ScalaMatsuri 2017](https://www.youtube.com/watch?v=Eyrz9AIzWXk). 6 | The game was ported to SGL as a proof of concept for the scala-native backend 7 | of SGL. 8 | 9 | The game comes with a configuration for export to the JVM desktop, the native 10 | desktop, and HTML5. The game was designed to be in a fixed squared window 11 | (20x20 tiles) and keyboard controls, which is not well adapted to mobile, hence 12 | we do not provide a configuration for Android export. 13 | 14 | You can try out the HTML5 version in your browser: 15 | [https://regb.github.io/scala-game-library/snake/](https://regb.github.io/scala-game-library/snake/) 16 | 17 | The [build definitions](../../build.sbt) are in the SGL root project 18 | definitions. If you have scala-native configured in your system, you should be 19 | able to run the native executable (from the root directory of SGL) with: 20 | 21 | sbt snakeDesktopNative/run 22 | 23 | The JVM-based desktop should work out of the box: 24 | 25 | sbt snakeDesktop/run 26 | 27 | You can build a local web version (one .js file): 28 | 29 | sbt snakeHtml5/fastOptJS 30 | -------------------------------------------------------------------------------- /examples/snake/core/src/main/scala/App.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.snake 2 | package core 3 | 4 | import sgl._ 5 | import sgl.proxy._ 6 | import sgl.util._ 7 | 8 | trait AbstractApp extends MainScreenComponent { 9 | this: GameApp => 10 | 11 | override def startingScreen: GameScreen = new MainScreen 12 | 13 | } 14 | 15 | object Wiring { 16 | 17 | def wire(platformProxy: PlatformProxy): ProxiedGameApp = { 18 | new AbstractApp with ProxyPlatformProvider { 19 | override val PlatformProxy: PlatformProxy = platformProxy 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/snake/core/src/main/scala/MainScreen.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.snake 2 | package core 3 | 4 | import sgl._ 5 | import geometry._ 6 | import scene._ 7 | import util._ 8 | 9 | trait MainScreenComponent { 10 | self: GraphicsProvider with GameStateComponent with WindowProvider 11 | with LoggingProvider with SystemProvider => 12 | 13 | import Graphics._ 14 | 15 | val NbRows = 30 16 | val NbCols = 30 17 | 18 | val squareSize = 20 19 | 20 | val TotalWidth = NbCols*squareSize 21 | val TotalHeight = NbRows*squareSize 22 | 23 | private implicit val LogTag = Logger.Tag("main-screen") 24 | 25 | class MainScreen extends FixedTimestepGameScreen(1000/12) { 26 | 27 | override def name: String = "SnakeScreen" 28 | 29 | var snake: List[Point] = Point(10, 10) :: Point(9, 10) :: Point(8, 10) :: Point(7, 10) :: Nil 30 | var rand = new java.util.Random 31 | var apple = newApple() 32 | 33 | val Up = Vec(0, -1) 34 | val Down = Vec(0, 1) 35 | val Left = Vec(-1, 0) 36 | val Right = Vec(1, 0) 37 | 38 | val snakeHeadPaint = defaultPaint.withColor(Color.Green) 39 | val snakePaint = defaultPaint.withColor(Color.Blue) 40 | val applePaint = defaultPaint.withColor(Color.Red) 41 | 42 | def gameOver(): Unit = { 43 | println("game over") 44 | gameState.newScreen(new MainScreen()) 45 | } 46 | 47 | def move(newPos: Point) = { 48 | if(newPos.x < 0 || newPos.y < 0 || newPos.x >= NbCols || newPos.y >= NbRows) { 49 | println("out of bounds") 50 | gameOver() 51 | } else if (snake.exists(_ == newPos)) { 52 | println("hit itself") 53 | gameOver() 54 | } else if (apple == newPos) { 55 | snake = newPos :: snake 56 | apple = newApple() 57 | } else { 58 | snake = newPos :: snake.init 59 | } 60 | } 61 | 62 | private var userDirection: Vec = snake(0) - snake(1) 63 | Input.setInputProcessor(new InputProcessor { 64 | override def keyDown(key: Input.Keys.Key): Boolean = { 65 | key match { 66 | case Input.Keys.Up => userDirection = Up 67 | case Input.Keys.Down => userDirection = Down 68 | case Input.Keys.Left => userDirection = Left 69 | case Input.Keys.Right => userDirection = Right 70 | case _ => () 71 | } 72 | true 73 | } 74 | }) 75 | 76 | override def fixedUpdate(): Unit = { 77 | val head :: second :: rest = snake 78 | val direction = head - second 79 | 80 | if(head + userDirection != second) 81 | move(head + userDirection) 82 | else 83 | move(head + direction) 84 | } 85 | 86 | 87 | def newApple(): Point = { 88 | var pos = Point(0, 0) 89 | do { 90 | pos = Point(rand.nextInt(NbCols).toFloat, rand.nextInt(NbRows).toFloat) 91 | } while (snake.exists(_ == pos)) 92 | pos 93 | } 94 | 95 | def drawSquare(canvas: Canvas, point: Point, paint: Paint) = { 96 | canvas.drawRect(point.x * squareSize, point.y * squareSize.toFloat, squareSize.toFloat, squareSize.toFloat, paint) 97 | } 98 | 99 | def drawSnake(canvas: Canvas): Unit = { 100 | val head :: tail = snake 101 | drawSquare(canvas, head, snakeHeadPaint) 102 | tail.foreach(sq => drawSquare(canvas, sq, snakePaint)) 103 | } 104 | def drawApple(canvas: Canvas): Unit = { 105 | drawSquare(canvas, apple, applePaint) 106 | } 107 | 108 | override def render(canvas: Canvas): Unit = { 109 | canvas.drawRect(0, 0, Window.width.toFloat, Window.height.toFloat, defaultPaint.withColor(Color.Black)) 110 | drawApple(canvas) 111 | drawSnake(canvas) 112 | } 113 | 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /examples/snake/desktop-awt/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.snake 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.{GameLoopStatisticsComponent} 7 | import sgl.awt._ 8 | import sgl.awt.util._ 9 | 10 | 11 | /** Wire backend to the App here */ 12 | object Main extends AWTApp with AbstractApp 13 | with VerboseStdErrLoggingProvider { 14 | 15 | override val TargetFps = Some(60) 16 | 17 | override val frameDimension = (TotalWidth, TotalHeight) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/snake/desktop-native/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.snake 2 | package desktop 3 | 4 | import core._ 5 | 6 | import sgl.GameLoopStatisticsComponent 7 | import sgl.native._ 8 | import sgl.native.util._ 9 | 10 | 11 | /** Wire backend to the App here */ 12 | object Main extends NativeApp with AbstractApp 13 | with VerboseStdErrLoggingProvider { 14 | 15 | override val TargetFps = Some(60) 16 | 17 | override val frameDimension = (TotalWidth, TotalHeight) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /examples/snake/html5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/snake/html5/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | package com.regblanc.sgl.snake 2 | package html5 3 | 4 | import sgl._ 5 | import sgl.scene._ 6 | import sgl.html5._ 7 | import sgl.html5.themes._ 8 | import sgl.util._ 9 | import sgl.html5.util._ 10 | 11 | import scala.scalajs.js.annotation.JSExport 12 | 13 | object Main extends Html5App with core.AbstractApp 14 | with Html5VerboseConsoleLoggingProvider { 15 | 16 | override val GameCanvasID: String = "my_canvas" 17 | 18 | //We should not force the fps on Html5 and just let 19 | //requestAnimationFrame do its best 20 | override val TargetFps: Option[Int] = None 21 | 22 | override val theme = new FixedWindowTheme { 23 | override val frameSize = (TotalWidth, TotalHeight) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /html5/cordova/src/main/scala/sgl/html5/CordovaApp.scala: -------------------------------------------------------------------------------- 1 | package sgl.html5 2 | 3 | import scala.scalajs.js 4 | 5 | // trait CordovaApp extends Html5App with CordovaMediaAudioProvider { 6 | trait CordovaApp extends Html5App with CordovaNativeAudioAudioProvider { 7 | 8 | override val Audio: Audio = CordovaNativeAudioAudio 9 | // override val Audio: Audio = CordovaMediaAudio 10 | 11 | override def main(args: Array[String]): Unit = { 12 | js.Dynamic.global.document.addEventListener("deviceready", () => { 13 | super.main(args) 14 | }) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /html5/cordova/src/main/scala/sgl/html5/analytics/CordovaFirebaseAnalyticsProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package html5 3 | package analytics 4 | 5 | import sgl.analytics._ 6 | 7 | import scala.scalajs.js 8 | 9 | /** An AnalyticsProvider using Firebase through Cordova. 10 | * 11 | * You will need to add the following plugin: 12 | * cordova plugin add cordova-plugin-firebase-analytics 13 | * 14 | * You will also need to add the google services config with the plugin: 15 | * cordova plugin add cordova-support-google-services 16 | * and adding the files on config.xml that you get from firebase when 17 | * configuring your app: 18 | * 19 | * 20 | * 21 | * ... 22 | * 23 | * 24 | * 25 | * ... 26 | * 27 | */ 28 | trait CordovaFirebaseAnalyticsProvider extends AnalyticsProvider { 29 | self: GameStateComponent => 30 | 31 | object CordovaFirebaseAnalytics extends Analytics { 32 | 33 | override def logCustomEvent(name: String, params: EventParams): Unit = { 34 | val dict = js.Dictionary.empty[Any] 35 | params.level.foreach(lvl => dict("level") = lvl.toDouble) 36 | params.value.foreach(v => dict("value") = v) 37 | params.itemId.foreach(id => dict("item_id") = id) 38 | params.score.foreach(s => dict("score") = s) 39 | params.levelName.foreach(m => dict("level_map") = m) 40 | params.character.foreach(c => dict("character") = c) 41 | params.customs.foreach{ case (k, v) => dict(k) = v } 42 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent(name, dict) 43 | } 44 | 45 | override def logLevelUpEvent(level: Long): Unit = { 46 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("level_up", js.Dynamic.literal("level" -> level.toDouble)) 47 | } 48 | 49 | override def logLevelEndEvent(level: String, success: Boolean): Unit = { 50 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("level_end", js.Dynamic.literal("level_name" -> level, "success" -> (if(success) 1 else 0))) 51 | } 52 | override def logLevelStartEvent(level: String): Unit = { 53 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("level_start", js.Dynamic.literal("level_name" -> level)) 54 | } 55 | 56 | override def logShareEvent(itemId: Option[String]): Unit = { 57 | itemId match { 58 | case None => js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("share" ) 59 | case Some(iid) => js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("share", js.Dynamic.literal("item_id" -> iid)) 60 | } 61 | } 62 | override def logGameOverEvent(score: Option[Long], map: Option[String]): Unit = { 63 | val params = js.Dictionary.empty[Any] 64 | score.foreach(s => params("score") = s.toDouble) 65 | map.foreach(m => params("level_map") = m) 66 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("game_over", params) 67 | } 68 | override def logBeginTutorialEvent(): Unit = { 69 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("begin_tutorial") 70 | } 71 | override def logCompleteTutorialEvent(): Unit = { 72 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("complete_tutorial") 73 | } 74 | 75 | override def logUnlockAchievementEvent(achievementId: String): Unit = { 76 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("unlock_achievement", js.Dynamic.literal("achievement_id" -> achievementId)) 77 | } 78 | 79 | override def logPostScoreEvent(score: Long, level: Option[Long], character: Option[String]): Unit = { 80 | val params = js.Dictionary[Any]("score" -> score.toDouble) 81 | level.foreach(l => params("level") = l.toDouble) 82 | character.foreach(c => params("character") = c) 83 | js.Dynamic.global.cordova.plugins.firebase.analytics.logEvent("post_score", params) 84 | } 85 | 86 | override def setGameScreen(gameScreen: GameScreen): Unit = { 87 | js.Dynamic.global.cordova.plugins.firebase.analytics.setCurrentScreen(gameScreen.name) 88 | } 89 | 90 | override def setPlayerProperty(name: String, value: String): Unit = { 91 | js.Dynamic.global.cordova.plugins.firebase.analytics.setUserProperty(name, value) 92 | } 93 | } 94 | 95 | override val Analytics: Analytics = CordovaFirebaseAnalytics 96 | 97 | } 98 | -------------------------------------------------------------------------------- /html5/firebase/src/main/scala/sgl/html5/firebase/Firebase.scala: -------------------------------------------------------------------------------- 1 | //import scala.scalajs.js 2 | //import scala.scalajs.js.annotation._ 3 | //import scala.scalajs.js.| 4 | // 5 | //package sgl.html5 { 6 | //package firebase { 7 | // 8 | // package app { 9 | // 10 | // @js.native 11 | // trait App extends js.Object { 12 | // def analytics(): firebase.analytics.Analytics = js.native 13 | // } 14 | // 15 | // } 16 | // 17 | // package analytics { 18 | // 19 | // @js.native 20 | // trait Analytics extends js.Object { 21 | // var app: firebase.app.App = js.native 22 | // 23 | // def logEvent(eventName: String, eventParams: Object*): Unit = js.native 24 | // } 25 | // 26 | // } 27 | // 28 | // @JSName("firebase") 29 | // @js.native 30 | // trait Firebase extends js.Object { 31 | // var SDK_VERSION: String = js.native 32 | // def app(name: String = ???): firebase.app.App = js.native 33 | // var apps: js.Array[firebase.app.App | Null] = js.native 34 | // def analytics(app: firebase.app.App = ???): firebase.analytics.Analytics = js.native 35 | // def initializeApp(options: FirebaseConfig, name: String = ???): firebase.app.App = js.native 36 | // } 37 | // 38 | // @JSExportAll 39 | // case class FirebaseConfig( 40 | // apiKey: String, 41 | // authDomain: String, 42 | // databaseURL: String, 43 | // storageBucket: String, 44 | // messagingSenderId: String 45 | // ) 46 | // 47 | //} 48 | //} 49 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/Html5SystemProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package html5 3 | 4 | import java.net.URI 5 | 6 | import org.scalajs.dom 7 | import scala.scalajs.js 8 | import js.typedarray.{ArrayBuffer, TypedArrayBuffer} 9 | 10 | import sgl.util._ 11 | 12 | trait Html5SystemProvider extends SystemProvider with PartsResourcePathProvider { 13 | 14 | object Html5System extends System { 15 | 16 | override def exit(): Unit = {} 17 | 18 | override def currentTimeMillis: Long = js.Date.now().toLong 19 | 20 | // Note that there is no way to get nanosecond precision in Javascript, so we 21 | // have to do with microsecond granularity. 22 | override def nanoTime: Long = (dom.window.performance.now()*1000L*1000L).toLong 23 | 24 | //probably cleaner to return lazily and block only when iterator is called 25 | //class LazyTextResource(rawFile: dom.XMLHttpRequest) extends Iterator[String] = { 26 | 27 | //} 28 | //but the best would be to redefine these loading APIs to be async 29 | 30 | override def loadText(path: ResourcePath): Loader[Array[String]] = { 31 | val p = new DefaultLoader[Array[String]]() 32 | val rawFile = new dom.XMLHttpRequest() 33 | rawFile.open("GET", path.path, true) 34 | rawFile.onreadystatechange = (event: dom.Event) => { 35 | if(rawFile.readyState == 4) { 36 | if(rawFile.status == 200 || rawFile.status == 0) { 37 | p.success(rawFile.responseText.split("\n").toArray) 38 | } else { 39 | p.failure(new RuntimeException("file: " + path + " failed to load")) 40 | } 41 | } 42 | } 43 | rawFile.send(null) 44 | p.loader 45 | } 46 | 47 | override def loadBinary(path: ResourcePath): Loader[Array[Byte]] = { 48 | val p = new DefaultLoader[Array[Byte]]() 49 | val fileReq = new dom.XMLHttpRequest() 50 | fileReq.open("GET", path.path, true) 51 | fileReq.responseType = "arraybuffer" 52 | fileReq.onreadystatechange = (event: dom.Event) => { 53 | if(fileReq.readyState == 4) { 54 | if(fileReq.status == 200 || fileReq.status == 0) { 55 | val responseBuffer: ArrayBuffer = fileReq.response.asInstanceOf[ArrayBuffer] 56 | val bb: java.nio.ByteBuffer = TypedArrayBuffer.wrap(responseBuffer) 57 | val array: Array[Byte] = new Array(bb.remaining) 58 | bb.get(array) 59 | p.success(array) 60 | } else { 61 | p.failure(new RuntimeException("file: " + path + " failed to load")) 62 | } 63 | } 64 | } 65 | fileReq.send(null) 66 | p.loader 67 | } 68 | 69 | override def openWebpage(uri: URI): Unit = { 70 | dom.window.open(uri.toString) 71 | } 72 | 73 | } 74 | val System = Html5System 75 | 76 | /** The root for all resources in an HTML5 game (Default to static/). 77 | * 78 | * All load* methods will search for resources starting in a static/ directory 79 | * at the same level as where the script is being executed. Typically the 80 | * script is going to be included by an HTML file, so say you have a layout as 81 | * follows: 82 | * 83 | * index.html 84 | * /game/index.html 85 | * /game/game.js 86 | * /game/static/drawable-mdpi 87 | * 88 | * And assuming the compiled game is in /game/game.js, and the script is 89 | * included in /game/index.html, the default implementation is going to 90 | * search for resources starting in /game/static/, because that's the 91 | * static/ directory at the same level as the point where the game is running. 92 | * 93 | * You can override this value to choose an arbitrary directory to look 94 | * for resources. This can be useful depending on your setup and how you 95 | * plan to deploy the web game. 96 | */ 97 | override val ResourcesRoot = PartsResourcePath(Vector("static")) 98 | final override val MultiDPIResourcesRoot = PartsResourcePath(Vector()) 99 | 100 | } 101 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/Html5WindowProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package html5 3 | 4 | import org.scalajs.dom 5 | 6 | trait Html5WindowProvider extends WindowProvider { 7 | self: Html5App => 8 | 9 | class Html5Window extends AbstractWindow { 10 | override def width: Int = self.htmlCanvas.width 11 | override def height: Int = self.htmlCanvas.height 12 | 13 | /* 14 | * On the web, pixels units are CSS pixels and are defined to be 1/96 of an 15 | * inch, when viewed from some angle. Essentially, when you refer to pixels 16 | * in Css, you are sort of guaranteed to get a consistent size on all screens 17 | * density (if the screen is twice as dense, your CSS pixel should use more 18 | * device pixel, and the visual result should be the same). In that sense, 19 | * this is a similar concept to DIP on Android, but such that you 20 | * can fit 96 CSS pixel an inch instead of 160 for DIP. 21 | * 22 | * The reason for this is the same as on Android, we want a consistent size 23 | * for UI items on all screen density. The reason to choose 96 is because 24 | * historically desktop monitor had a density of 96 pixel per inch. 25 | * 26 | * The concept of devicePixelRatio was introduced to be a multiplier to go 27 | * from CSS pixel to device pixels in new high dpi mobile screens. Unfortunately, 28 | * the browsers will usually not export the exact pixel ratio, instead they will 29 | * export a convenient estimate like 1.0, 2.0, 3.0. This means we cannot 30 | * get the exact ppi with these. 31 | * 32 | * The estimate is still useful, we know that if the pixel ratio is 1.0, we 33 | * have a low density screen (like a regular desktop or old mobile), if 34 | * we have a 2.0, we have a high density (like a retina display or latest 35 | * mobile screens). 36 | * 37 | * Although we could get to a rough ppi by multiplying the devicePixelRatio 38 | * by the 96, it turns out that the exact definition is 96 pixels per 39 | * CSS inch, which is not exactly an inch, but rather an inch depending 40 | * on the angle of vision. While on Desktop it would roughly be 96, on a 41 | * mobile, where users are closer, it will be closer to 150. All in all, 42 | * the value we export for ppi is kind of arbitrary. 43 | * 44 | * However, we want to ensure a consistent appearence for games across all 45 | * platforms, and since some games might make use of these values in order 46 | * to convert from a theoretical pixel to a physical pixel before drawing 47 | * graphics, it is important that all these measures are consistent. In 48 | * particular, we need to ensure that when loading a bitmap image, its 49 | * dimensions are consistent with the ppi returned here (if we load an mdpi 50 | * image, and it's not scaled, then the ppi should be 160 and nothing else, 51 | * if we load an mdpi image, and the ppi is 320 (xhdpi), we must scale the 52 | * mdpi image x2). 53 | * 54 | * Given these constraints, it seems best to match one CSS pixel to the 55 | * standard DPI from Android, which is 1/160 of an inch. That means that 56 | * the default ppi will be 160, and it will be mulitplied by the 57 | * devicePixelRatio. 58 | */ 59 | override def xppi: Float = (160*dom.window.devicePixelRatio).toFloat 60 | override def yppi: Float = (160*dom.window.devicePixelRatio).toFloat 61 | 62 | /* 63 | * The above comment is also relevant ot the logicalPpi, but essentially 64 | * in this case the devicePixelRatio is exactly the meaning of a logical 65 | * ppi, because it's not exact, but it's a convenient value to use. We 66 | * still need to arbitrarily bring it to a 160 base though. 67 | */ 68 | override def logicalPpi: Float = (160*dom.window.devicePixelRatio).toFloat 69 | 70 | } 71 | type Window = Html5Window 72 | override val Window = new Html5Window 73 | 74 | } 75 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/LocalStorageSave.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package html5 3 | 4 | import scala.scalajs.js 5 | import org.scalajs.dom 6 | 7 | /** Implement Save using the local storage API. */ 8 | object LocalStorageSave extends AbstractSave { 9 | 10 | val localStorageSupported = !js.isUndefined(js.Dynamic.global.localStorage) 11 | 12 | // One idea could be to fallback on cookies for the implementation, although 13 | // I think we should be able to assume support for local storage, as our other 14 | // dependencies are probably stronger. 15 | 16 | override def putString(name: String, value: String): Unit = { 17 | if(localStorageSupported) { 18 | dom.window.localStorage.setItem(name, value) 19 | } 20 | } 21 | override def getString(name: String): Option[String] = { 22 | if(localStorageSupported) { 23 | val res = dom.window.localStorage.getItem(name) 24 | if(res == null) None else Some(res) 25 | } else None 26 | } 27 | 28 | override def putInt(name: String, value: Int): Unit = { 29 | putString(name, value.toString) 30 | } 31 | 32 | override def getInt(name: String): Option[Int] = { 33 | getString(name).flatMap(v => try { 34 | Some(v.toInt) 35 | } catch { 36 | case (_: Exception) => None 37 | }) 38 | } 39 | 40 | override def putLong(name: String, value: Long): Unit = { 41 | putString(name, value.toString) 42 | } 43 | 44 | override def getLong(name: String): Option[Long] = { 45 | getString(name).flatMap(v => try { 46 | Some(v.toLong) 47 | } catch { 48 | case (_: Exception) => None 49 | }) 50 | } 51 | 52 | override def putBoolean(name: String, value: Boolean): Unit = { 53 | putString(name, value.toString) 54 | } 55 | override def getBoolean(name: String): Option[Boolean] = { 56 | getString(name).flatMap(v => try { 57 | Some(v.toBoolean) 58 | } catch { 59 | case (_: Exception) => None 60 | }) 61 | } 62 | 63 | } 64 | 65 | trait LocalStorageSaveComponent extends SaveComponent { 66 | type Save = LocalStorageSave.type 67 | override val Save = LocalStorageSave 68 | } 69 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/themes/FixedWindowTheme.scala: -------------------------------------------------------------------------------- 1 | package sgl.html5 2 | package themes 3 | 4 | import org.scalajs.dom 5 | import dom.html 6 | 7 | /** The window has a fixed size in pixels. 8 | * 9 | * The window is not adapted to the client browser, but just sits centered if 10 | * there is enough space, otherwise it will overflow and require scrolling 11 | */ 12 | trait FixedWindowTheme extends Theme { 13 | 14 | /** Override the background color behind the canvas game. */ 15 | val backgroundColor: String = "rgb(42,42,42)" 16 | 17 | /** The canvas always use these (width,height) (in CSS pixels). */ 18 | val frameSize: (Int, Int) 19 | 20 | override def init(canvas: html.Canvas): Unit = { 21 | dom.document.body.style.backgroundColor = backgroundColor 22 | dom.document.body.style.margin = "0" 23 | dom.document.body.style.padding = "0" 24 | 25 | // prevent highlight on click on canvas. 26 | dom.document.onselectstart = (e: dom.Event) => false 27 | 28 | canvas.style.margin = "0" 29 | canvas.style.padding = "0" 30 | canvas.style.display = "block" 31 | canvas.style.position = "absolute" 32 | 33 | canvas.width = frameSize._1 34 | canvas.height = frameSize._2 35 | setPosition(canvas) 36 | } 37 | 38 | override def onResize(canvas: html.Canvas): Unit = { 39 | // It's not great, but some other parts of the backend might change the width/height, 40 | // so we need to reset them here. Would be good to have something cleaner, but 41 | // not sure how. 42 | canvas.width = frameSize._1 43 | canvas.height = frameSize._2 44 | setPosition(canvas) 45 | } 46 | 47 | /* Automatically center dynamically. */ 48 | private def setPosition(canvas: html.Canvas): Unit = { 49 | /* 50 | * We use body.clientWidth/clientHeight to take into account the potential 51 | * space used by scroll bars. 52 | */ 53 | val windowWidth = dom.document.body.clientWidth.toInt 54 | val windowHeight = dom.document.body.clientHeight.toInt 55 | 56 | if(windowWidth < frameSize._1) 57 | canvas.style.left = "0" 58 | else { 59 | val left: Int = (windowWidth - canvas.width)/2 60 | canvas.style.left = left + "px" 61 | } 62 | 63 | if(windowHeight < frameSize._2) 64 | canvas.style.top = "0" 65 | else { 66 | val top: Int = (windowHeight - canvas.height)/2 67 | canvas.style.top = top + "px" 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/themes/FullScreenTheme.scala: -------------------------------------------------------------------------------- 1 | package sgl.html5 2 | package themes 3 | 4 | import org.scalajs.dom 5 | import dom.html 6 | 7 | /** The canvas is always using the full screen available. 8 | * 9 | * This probably makes the most sense for a native app using 10 | * a webview around the game. The other use case might be when 11 | * you want to handle everything in the canvas (with code that adapts 12 | * to the available space, and for serious desktop games with a 13 | * web mode). 14 | */ 15 | class FullScreenTheme extends Theme { 16 | 17 | override def init(canvas: html.Canvas): Unit = { 18 | dom.document.body.style.margin = "0" 19 | dom.document.body.style.padding = "0" 20 | dom.document.body.style.overflow = "hidden" 21 | 22 | // prevent highlight on click on canvas. 23 | dom.document.onselectstart = (e: dom.Event) => false 24 | 25 | canvas.style.margin = "0" 26 | canvas.style.padding = "0" 27 | canvas.style.display = "block" 28 | canvas.style.position = "absolute" 29 | canvas.style.left = "0" 30 | canvas.style.top = "0" 31 | 32 | canvas.width = dom.window.innerWidth.toInt 33 | canvas.height = dom.window.innerHeight.toInt 34 | } 35 | 36 | override def onResize(canvas: html.Canvas): Unit = { 37 | canvas.width = dom.window.innerWidth.toInt 38 | canvas.height = dom.window.innerHeight.toInt 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/themes/Theme.scala: -------------------------------------------------------------------------------- 1 | package sgl.html5 2 | package themes 3 | 4 | import org.scalajs.dom 5 | import dom.html 6 | 7 | /** Specify how the canvas app interact with the web page. 8 | * 9 | * This is some re-usable bit of code, that can be configured when setting up 10 | * the HTML5 app. It's only relevant to html5 based games, so the interface is 11 | * not visible to the rest of the SGL. 12 | * 13 | * The general idea of the SGL is to not be concerned with things such as 14 | * windowing or containers, and only expose a canvas to be drawn on. This 15 | * means that deployment questions such as frame dimension, fullscreen or not, 16 | * are left to the backend wiring. The idea of providing a theme is to help 17 | * package standard website for html5 games. 18 | * 19 | * We define the theme as an external object, as we didn't wanted to mix the 20 | * decision of where to display the canvas, with the actual SGL code that 21 | * handle the content of the canvas. Having an external theme, with a NoTheme 22 | * option as well, let the user decides exactly how he wishes to inject a 23 | * canvas game into his website. 24 | */ 25 | abstract class Theme { 26 | 27 | /** Initialize the canvas with this theme. 28 | * 29 | * The function is called once only, at startup, before any others and 30 | * before any particular processing has been done on the canvas. 31 | **/ 32 | def init(canvas: html.Canvas): Unit 33 | 34 | /** Called when the browser window is resized. 35 | * 36 | * This is just forwarding the onresize event of the browser, the theme 37 | * should take advantage of this to update anything that needs to 38 | * be updated to maintain the canvas. 39 | */ 40 | def onResize(canvas: html.Canvas): Unit 41 | 42 | } 43 | 44 | /** Do not use any special theme. 45 | * 46 | * This theme is useful if we want the game to just use the 47 | * provided canvas element (not moving it or mutating its dimensions). It 48 | * gives full control to the html/css designer that can setup the 49 | * optimal environment for the game. 50 | */ 51 | class NoTheme extends Theme { 52 | 53 | override def init(canvas: html.Canvas): Unit = {} 54 | 55 | override def onResize(canvas: html.Canvas): Unit = {} 56 | 57 | } 58 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/util/Html5ConsoleLoggingProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl.html5 2 | package util 3 | 4 | import sgl.util._ 5 | 6 | import org.scalajs.dom 7 | 8 | /* 9 | * Note that it seems scala.js does nicely eliminate all overhead 10 | * of logging call when using NoLoggingProvider (the empty logger). 11 | * It is nice, as we pass everything by value, the string are not 12 | * even constructed so the logging framework is basically free 13 | * in production. 14 | */ 15 | 16 | trait Html5ConsoleLoggingProvider extends LoggingProvider { 17 | 18 | import Logger._ 19 | 20 | abstract class ConsoleLogger extends Logger { 21 | private def format(tag: Tag, msg: String): String = { 22 | val prefix = s"[ ${tag.name} ]" 23 | val alignedMsg = msg.trim.replaceAll("\n", "\n" + (" " * prefix.length)) 24 | s"${prefix} $alignedMsg" 25 | } 26 | 27 | override protected def log(level: LogLevel, tag: Tag, msg: String): Unit = level match { 28 | case NoLogging => () 29 | case Error => dom.console.error(format(tag, msg)) 30 | case Warning => dom.console.warn(format(tag, msg)) 31 | case Info => dom.console.info(format(tag, msg)) 32 | case Debug => dom.console.log(format(tag, msg)) 33 | case Trace => dom.console.log(format(tag, msg)) 34 | } 35 | } 36 | } 37 | 38 | 39 | trait Html5DefaultConsoleLoggingProvider extends Html5ConsoleLoggingProvider { 40 | case object DefaultConsoleLogger extends ConsoleLogger { 41 | override val logLevel: Logger.LogLevel = Logger.Warning 42 | } 43 | override val logger = DefaultConsoleLogger 44 | } 45 | 46 | trait Html5InfoConsoleLoggingProvider extends Html5ConsoleLoggingProvider { 47 | case object InfoConsoleLogger extends ConsoleLogger { 48 | override val logLevel: Logger.LogLevel = Logger.Info 49 | } 50 | override val logger = InfoConsoleLogger 51 | } 52 | 53 | trait Html5VerboseConsoleLoggingProvider extends Html5ConsoleLoggingProvider { 54 | case object VerboseConsoleLogger extends ConsoleLogger { 55 | override val logLevel: Logger.LogLevel = Logger.Debug 56 | } 57 | override val logger = VerboseConsoleLogger 58 | } 59 | -------------------------------------------------------------------------------- /html5/src/main/scala/sgl/html5/util/Html5JsonProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl 2 | package html5 3 | package util 4 | 5 | import sgl.util.JsonProvider 6 | 7 | import scala.scalajs.js 8 | 9 | import scala.language.implicitConversions 10 | 11 | trait Html5JsonProvider extends JsonProvider { 12 | 13 | object Html5Json extends Json { 14 | 15 | type JValue = Any 16 | override def parse(raw: String): JValue = { 17 | try { 18 | js.JSON.parse(raw) 19 | } catch { 20 | case (e: js.JavaScriptException) => 21 | throw new ParseException(e.getMessage) 22 | } 23 | } 24 | 25 | class Html5RichJsonAst(v: JValue) extends RichJsonAst { 26 | override def \ (field: String): JValue = v.asInstanceOf[js.Dynamic].selectDynamic(field) 27 | } 28 | override implicit def richJsonAst(ast: JValue) = new Html5RichJsonAst(ast) 29 | 30 | type JNothing = Unit 31 | override val JNothing = () 32 | type JNull = Null 33 | override val JNull = null 34 | 35 | object Html5JStringCompanion extends JStringCompanion { 36 | override def unapply(v: JValue): Option[String] = v match { 37 | case (x: String) => Some(x) 38 | case _ => None 39 | } 40 | } 41 | type JString = String 42 | override val JString = Html5JStringCompanion 43 | 44 | object Html5JNumberCompanion extends JNumberCompanion { 45 | override def unapply(v: JValue): Option[Double] = v match { 46 | case (x: Int) => Some(x) 47 | case (x: Double) => Some(x) 48 | case _ => None 49 | } 50 | } 51 | type JNumber = Double 52 | override val JNumber = Html5JNumberCompanion 53 | 54 | object Html5JBooleanCompanion extends JBooleanCompanion { 55 | override def unapply(v: JValue): Option[Boolean] = v match { 56 | case (x: Boolean) => Some(x) 57 | case _ => None 58 | } 59 | } 60 | type JBoolean = Boolean 61 | override val JBoolean = Html5JBooleanCompanion 62 | 63 | object Html5JObjectCompanion extends JObjectCompanion { 64 | override def unapply(v: JValue): Option[List[JField]] = { 65 | if(v.toString == "[object Object]") { 66 | val d = v.asInstanceOf[js.Dictionary[Any]] 67 | Some((for ((k, v) <- d) yield (k, v)).toList) 68 | } else None 69 | } 70 | } 71 | type JObject = js.Dictionary[Any] 72 | override val JObject = Html5JObjectCompanion 73 | 74 | object Html5JArrayCompanion extends JArrayCompanion { 75 | override def unapply(v: JValue): Option[List[JValue]] = v match { 76 | case (x: js.Array[_]) => Some(x.map((x: Any) => x).toList) 77 | case _ => None 78 | } 79 | } 80 | type JArray = js.Array[Any] 81 | override val JArray = Html5JArrayCompanion 82 | } 83 | 84 | override val Json = Html5Json 85 | 86 | } 87 | -------------------------------------------------------------------------------- /html5/src/test/scala/sgl/html5/util/Html5JsonProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl.html5.util 2 | 3 | import sgl.util.JsonProviderAbstractSuite 4 | 5 | class Html5JsonProviderSuite extends JsonProviderAbstractSuite with Html5JsonProvider 6 | -------------------------------------------------------------------------------- /jvm-shared/src/main/scala/sgl/util/FutureLoader.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import scala.concurrent.Future 4 | import scala.concurrent.ExecutionContext 5 | 6 | /** A Loader backed by a Future. 7 | * 8 | * We use Future infrastructure to perform the asynchronous loading on 9 | * JVM-based platform. This is not used on other platforms, as we want to have 10 | * a better integration in the system, but it seems like a good choice 11 | * for JVM-based platform with standard threads. 12 | */ 13 | object FutureLoader { 14 | 15 | def apply[A](body: => A)(implicit ec: ExecutionContext): Loader[A] = { 16 | val loader = new DefaultLoader[A] 17 | val f = Future(body) 18 | f.onComplete(r => loader.complete(r)) 19 | loader.loader 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /jvm-shared/src/main/scala/sgl/util/ThreadPoolSchedulerProvider.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import java.util.concurrent.Executors 4 | import java.io.{StringWriter, PrintWriter} 5 | import scala.collection.mutable.Queue 6 | 7 | trait ThreadPoolSchedulerProvider extends SchedulerProvider { 8 | this: LoggingProvider => 9 | 10 | private implicit val Tag = Logger.Tag("threadpool-scheduler") 11 | 12 | class ThreadPoolScheduler extends Scheduler { 13 | private val pool = Executors.newFixedThreadPool(4) 14 | 15 | private val tasks: Queue[ChunkedTask] = new Queue 16 | private val taskQueueLock = new Object 17 | 18 | private var r1: ChunksRunner = null 19 | private var r2: ChunksRunner = null 20 | private var r3: ChunksRunner = null 21 | private var r4: ChunksRunner = null 22 | 23 | override def schedule(task: ChunkedTask): Unit = { 24 | taskQueueLock.synchronized { 25 | tasks.enqueue(task) 26 | } 27 | } 28 | 29 | /** Resume the execution of all scheduled task. 30 | * 31 | * This method should always be called after creating the 32 | * Scheduler. This helps in maintaining the symetry with pause(). 33 | */ 34 | def resume(): Unit = { 35 | r1 = new ChunksRunner 36 | r2 = new ChunksRunner 37 | r3 = new ChunksRunner 38 | r4 = new ChunksRunner 39 | pool.submit(r1) 40 | pool.submit(r2) 41 | pool.submit(r3) 42 | pool.submit(r4) 43 | } 44 | 45 | /** Pause the execution of all scheduled task. 46 | * 47 | * This will prevent all worker threads from doing work but 48 | * will not release them. 49 | */ 50 | def pause(): Unit = { 51 | r1.shouldStop = true 52 | r2.shouldStop = true 53 | r3.shouldStop = true 54 | r4.shouldStop = true 55 | } 56 | 57 | /** The Scheduler will stop executing all scheduled task 58 | * and it will clean-up all platform-specific resources 59 | * (for example, running worker threads). 60 | * It is ok to call shutdown before any call to resume() and/or 61 | * schedule(), which means one can create a scheduler and then 62 | * right away shutdown on it. 63 | */ 64 | def shutdown(): Unit = { 65 | pool.shutdown() 66 | 67 | // Need to check for null because we could have skipped resume. 68 | if(r1 != null) 69 | r1.shouldStop = true 70 | if(r2 != null) 71 | r2.shouldStop = true 72 | if(r3 != null) 73 | r3.shouldStop = true 74 | if(r4 != null) 75 | r4.shouldStop = true 76 | } 77 | 78 | // Simple Runnable class that picks up the first available ChunkedTask and 79 | // run one chunk of it. 80 | // Note that if there is only one ChunkedTask in the queue, there will only 81 | // be one busy Thread at a time as ChunkedTask are assumed to be sequentials. 82 | // In order to optimize the use of the thread pool, one should try to split 83 | // parallel work into several independent ChunkedTask. 84 | class ChunksRunner extends Runnable { 85 | var shouldStop = false 86 | override def run(): Unit = { 87 | while(!shouldStop) { 88 | val task = taskQueueLock.synchronized { 89 | if(tasks.isEmpty) { 90 | None 91 | } else { 92 | Some(tasks.dequeue()) 93 | } 94 | } 95 | task match { 96 | case None => Thread.sleep(50) 97 | case Some(task) => { 98 | logger.debug("Executing some ChunkedTask from the task queue.") 99 | try { 100 | task.doRun(5l) 101 | if(task.status != ChunkedTask.Completed) 102 | taskQueueLock.synchronized { tasks.enqueue(task) } 103 | } catch { 104 | case (e: Throwable) => { 105 | logger.error(s"Unexpected error while executing task ${task.name}: ${e.getMessage}") 106 | val sw = new StringWriter() 107 | val pw = new PrintWriter(sw, true) 108 | e.printStackTrace(pw) 109 | logger.error(sw.toString) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | override val Scheduler = new ThreadPoolScheduler 119 | 120 | } 121 | -------------------------------------------------------------------------------- /jvm-shared/src/test/scala/sgl/util/FutureLoaderSuite.scala: -------------------------------------------------------------------------------- 1 | package sgl.util 2 | 3 | import scala.concurrent.ExecutionContext.Implicits.global 4 | 5 | class FutureLoaderSuite extends LoaderAbstractSuite { 6 | 7 | override def makeLoader[A](body: => A): Loader[A] = FutureLoader(body) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.4.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") 2 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.0.0") 3 | 4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1") 5 | 6 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.0") 7 | --------------------------------------------------------------------------------