├── .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 |
--------------------------------------------------------------------------------