19 |
20 | /* This is a trivial JNI example where we use a native method
21 | * to return a new VM String. See the corresponding Java source
22 | * file located at:
23 | *
24 | * apps/samples/hello-jni/project/src/com/example/hellojni/HelloJni.java
25 | */
26 | jstring
27 | Java_games_demoAndroid_HelloJni_stringFromJNI( JNIEnv* env,
28 | jobject thiz )
29 | {
30 | #if defined(__arm__)
31 | #if defined(__ARM_ARCH_7A__)
32 | #if defined(__ARM_NEON__)
33 | #define ABI "armeabi-v7a/NEON"
34 | #else
35 | #define ABI "armeabi-v7a"
36 | #endif
37 | #else
38 | #define ABI "armeabi"
39 | #endif
40 | #elif defined(__i386__)
41 | #define ABI "x86"
42 | #elif defined(__mips__)
43 | #define ABI "mips"
44 | #else
45 | #define ABI "unknown"
46 | #endif
47 |
48 | return (*env)->NewStringUTF(env, "Hello from JNI ! Compiled with ABI " ABI ".");
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/demo/android/src/main/libs/arm64-v8a/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/arm64-v8a/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/libs/armeabi-v7a/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/armeabi-v7a/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/libs/armeabi/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/armeabi/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/libs/mips/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/mips/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/libs/mips64/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/mips64/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/libs/x86/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/x86/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/libs/x86_64/libprecompiled-jni.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/android/src/main/libs/x86_64/libprecompiled-jni.so
--------------------------------------------------------------------------------
/demo/android/src/main/scala/games/demo/Specifics.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | object Specifics {
4 | type WebSocketClient = transport.tyrus.WebSocketClient
5 | val platformName = "Android"
6 | }
7 |
--------------------------------------------------------------------------------
/demo/android/src/main/scala/games/demoAndroid/Launcher.scala:
--------------------------------------------------------------------------------
1 | package games.demoAndroid
2 |
3 | import android.os.Bundle
4 | import android.app.Activity
5 | import android.widget.TextView
6 |
7 | import java.lang.Runnable
8 |
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 | import scala.concurrent.Future
11 | import games.demo.Engine
12 |
13 | class Launcher extends Activity {
14 | override def onCreate(savedInstanceState: Bundle) = {
15 | super.onCreate(savedInstanceState)
16 | val tv = new TextView(this)
17 | setContentView(tv)
18 | var text: List[String] = Nil
19 | def printTextViewLine(s: String) {
20 | runOnUiThread(new Runnable {
21 | @Override def run(): Unit = {
22 | text = s :: text
23 | tv.setText(text.reverse.mkString("\n"))
24 | }
25 | })
26 | }
27 |
28 | val jni = new HelloJni
29 | printTextViewLine("Test jni: " + jni.stringFromJNI())
30 |
31 | val prejni = new PrecompiledJni
32 | printTextViewLine("Test precompiled jni: " + prejni.precompiledStringFromJNI())
33 |
34 | Future { // Android does not like IO on the UI thread
35 | val engine = new Engine(printTextViewLine)
36 | engine.start()
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/Utils.scala:
--------------------------------------------------------------------------------
1 | package games
2 |
3 | import scala.concurrent.{ Future, Promise, ExecutionContext }
4 | import scala.scalajs.js
5 | import js.Dynamic.{ global => g }
6 | import org.scalajs.dom
7 | import java.nio.ByteBuffer
8 | import games.opengl.GLES2
9 | import games.opengl.GLES2WebGL
10 | import games.opengl.GLES2Debug
11 | import scala.collection.mutable.Queue
12 |
13 | object JsUtils {
14 | private val userEventTasks: Queue[Runnable] = Queue()
15 |
16 | def flushUserEventTasks(): Unit = {
17 | userEventTasks.foreach { runnable =>
18 | try {
19 | runnable.run()
20 | } catch {
21 | case t: Throwable => userEventExecutionContext.reportFailure(t)
22 | }
23 | }
24 |
25 | userEventTasks.clear()
26 | }
27 |
28 | val userEventExecutionContext: ExecutionContext = new ExecutionContext() {
29 | def execute(runnable: Runnable): Unit = userEventTasks += runnable
30 | def reportFailure(cause: Throwable): Unit = ExecutionContext.defaultReporter(cause)
31 | }
32 |
33 | private var relativeResourcePath: Option[String] = None
34 |
35 | private[games] def pathForResource(res: Resource): String = relativeResourcePath match {
36 | case Some(path) => path + res.name
37 | case None => throw new RuntimeException("Relative path must be defined before calling pathForResource")
38 | }
39 |
40 | // TODO performance.now() for microseconds precision: https://developer.mozilla.org/en-US/docs/Web/API/Performance.now%28%29
41 | private[games] def now(): Double = g.Date.now().asInstanceOf[Double]
42 |
43 | private[games] def getWebGLRenderingContext(gl: GLES2): dom.webgl.RenderingContext = gl match {
44 | case gles2webgl: GLES2WebGL => gles2webgl.getWebGLRenderingContext()
45 | case gles2debug: GLES2Debug => getWebGLRenderingContext(gles2debug.getInternalContext())
46 | case _ => throw new RuntimeException("Could not retrieve the WebGLRenderingContext from GLES2")
47 | }
48 |
49 | private[games] def getOptional[T](el: js.Dynamic, fields: String*): Option[T] = {
50 | def getOptionalJS(fields: String*): js.UndefOr[T] = {
51 | if (fields.isEmpty) js.undefined
52 | else el.selectDynamic(fields.head).asInstanceOf[js.UndefOr[T]].orElse(getOptionalJS(fields.tail: _*))
53 | }
54 |
55 | getOptionalJS(fields: _*).toOption
56 | }
57 |
58 | private[games] def featureUnsupportedText(feature: String): String = {
59 | "Feature " + feature + " not supported"
60 | }
61 |
62 | private[games] def featureUnsupportedFunction(feature: String): js.Function = {
63 | () => { Console.err.println(featureUnsupportedText(feature)) }
64 | }
65 |
66 | private[games] def throwFeatureUnsupported(feature: String): Nothing = {
67 | throw new RuntimeException(featureUnsupportedText(feature))
68 | }
69 |
70 | private val typeRegex = js.Dynamic.newInstance(g.RegExp)("^\\[object\\s(.*)\\]$")
71 |
72 | /*
73 | * Return the type of the JavaScript object as a String. Examples:
74 | * 1.5 -> Number
75 | * true -> Boolean
76 | * "Hello" -> String
77 | * null -> Null
78 | */
79 | private[games] def typeName(jsObj: js.Any): String = {
80 | val fullName = g.Object.prototype.selectDynamic("toString").call(jsObj).asInstanceOf[String]
81 | val execArray = typeRegex.exec(fullName).asInstanceOf[js.Array[String]]
82 | val name = execArray(1)
83 | name
84 | }
85 |
86 | /*
87 | * Get the offset of the element.
88 | * From jQuery: https://github.com/jquery/jquery/blob/2.1.3/src/offset.js#L107-L108
89 | */
90 | private[games] def offsetOfElement(element: js.Any): (Int, Int) = if (element == dom.document) {
91 | (0, 0)
92 | } else {
93 | val dynElement = element.asInstanceOf[js.Dynamic]
94 |
95 | val bounding = dynElement.getBoundingClientRect()
96 | val window = js.Dynamic.global.window
97 |
98 | val boundingLeft = bounding.left.asInstanceOf[Double]
99 | val boundingTop = bounding.top.asInstanceOf[Double]
100 |
101 | val winOffsetX = window.pageXOffset.asInstanceOf[Double]
102 | val winOffsetY = window.pageYOffset.asInstanceOf[Double]
103 |
104 | val elemOffsetX = dynElement.clientLeft.asInstanceOf[Double]
105 | val elemOffsetY = dynElement.clientTop.asInstanceOf[Double]
106 |
107 | ((boundingLeft + winOffsetX - elemOffsetX).toInt, (boundingTop + winOffsetY - elemOffsetY).toInt)
108 | }
109 |
110 | private[games] object Browser {
111 | private val userAgent: String = js.Dynamic.global.navigator.userAgent.asInstanceOf[String].toLowerCase()
112 |
113 | val chrome: Boolean = userAgent.contains("chrome/")
114 | val firefox: Boolean = userAgent.contains("firefox/")
115 | val android: Boolean = userAgent.contains("android")
116 | }
117 |
118 | def setResourcePath(path: String): Unit = {
119 | relativeResourcePath = Some(path)
120 | }
121 |
122 | var autoToggling: Boolean = false
123 | var orientationLockOnFullscreen: Boolean = false
124 | var useAuroraJs: Boolean = true
125 | }
126 |
127 | trait UtilsImpl extends UtilsRequirements {
128 | private[games] def getLoopThreadExecutionContext(): ExecutionContext = scalajs.concurrent.JSExecutionContext.Implicits.queue
129 |
130 | private def isHTTPCodeOk(code: Int): Boolean = (code >= 200 && code < 300) || code == 304 // HTTP Code 2xx or 304, Ok
131 |
132 | def getBinaryDataFromResource(res: games.Resource)(implicit ec: ExecutionContext): scala.concurrent.Future[java.nio.ByteBuffer] = {
133 | val xmlRequest = new dom.XMLHttpRequest()
134 |
135 | val path = JsUtils.pathForResource(res)
136 |
137 | xmlRequest.open("GET", path, true)
138 | xmlRequest.responseType = "arraybuffer"
139 | xmlRequest.asInstanceOf[js.Dynamic].overrideMimeType("application/octet-stream")
140 |
141 | val promise = Promise[ByteBuffer]
142 |
143 | def error(): String = "Could not binary text resource " + res + ": code " + xmlRequest.status + " (" + xmlRequest.statusText + ")"
144 |
145 | xmlRequest.onload = (e: dom.Event) => {
146 | val code = xmlRequest.status
147 | if (isHTTPCodeOk(code)) {
148 | val arrayBuffer = xmlRequest.response.asInstanceOf[js.typedarray.ArrayBuffer]
149 | val byteBuffer = js.typedarray.TypedArrayBuffer.wrap(arrayBuffer)
150 | promise.success(byteBuffer)
151 | } else {
152 | promise.failure(new RuntimeException(error()))
153 | }
154 | }
155 | xmlRequest.onerror = (e: dom.Event) => {
156 | promise.failure(new RuntimeException(error()))
157 | }
158 |
159 | xmlRequest.send(null)
160 |
161 | promise.future
162 | }
163 |
164 | def getTextDataFromResource(res: games.Resource)(implicit ec: ExecutionContext): scala.concurrent.Future[String] = {
165 | val xmlRequest = new dom.XMLHttpRequest()
166 |
167 | val path = JsUtils.pathForResource(res)
168 |
169 | xmlRequest.open("GET", path, true)
170 | xmlRequest.responseType = "text"
171 | xmlRequest.asInstanceOf[js.Dynamic].overrideMimeType("text/plain")
172 |
173 | val promise = Promise[String]
174 |
175 | def error(): String = "Could not retrieve text resource " + res + ": code " + xmlRequest.status + " (" + xmlRequest.statusText + ")"
176 |
177 | xmlRequest.onload = (e: dom.Event) => {
178 | val code = xmlRequest.status
179 | if (isHTTPCodeOk(code)) { // HTTP Code 2xx or 304, Ok
180 | val text: String = xmlRequest.responseText
181 | promise.success(text)
182 | } else {
183 | promise.failure(new RuntimeException(error()))
184 | }
185 | }
186 | xmlRequest.onerror = (e: dom.Event) => {
187 | promise.failure(new RuntimeException(error()))
188 | }
189 |
190 | xmlRequest.send(null)
191 |
192 | promise.future
193 | }
194 | def loadTexture2DFromResource(res: games.Resource, texture: games.opengl.Token.Texture, gl: games.opengl.GLES2, openglExecutionContext: ExecutionContext)(implicit ec: ExecutionContext): scala.concurrent.Future[Unit] = {
195 | val image = dom.document.createElement("img").asInstanceOf[js.Dynamic]
196 |
197 | val promise = Promise[Unit]
198 |
199 | image.onload = () => {
200 | try {
201 | val previousTexture = gl.getParameterTexture(GLES2.TEXTURE_BINDING_2D)
202 | gl.bindTexture(GLES2.TEXTURE_2D, texture)
203 |
204 | val webglRenderingContext = JsUtils.getWebGLRenderingContext(gl)
205 | val webglCtx = webglRenderingContext.asInstanceOf[js.Dynamic]
206 | webglCtx.pixelStorei(webglCtx.UNPACK_FLIP_Y_WEBGL, false)
207 | webglRenderingContext.texImage2D(GLES2.TEXTURE_2D, 0, GLES2.RGBA, GLES2.RGBA, GLES2.UNSIGNED_BYTE, image.asInstanceOf[dom.html.Image])
208 | gl.bindTexture(GLES2.TEXTURE_2D, previousTexture)
209 |
210 | promise.success((): Unit)
211 | } catch {
212 | case t: Throwable => promise.failure(t)
213 | }
214 | }
215 | image.onerror = () => {
216 | promise.failure(new RuntimeException("Could not retrieve image " + res))
217 | }
218 |
219 | image.src = JsUtils.pathForResource(res)
220 |
221 | promise.future
222 | }
223 | def startFrameListener(fl: games.FrameListener): Unit = {
224 | class FrameListenerLoopContext {
225 | var lastLoopTime: Double = JsUtils.now()
226 | var closed: Boolean = false
227 | }
228 |
229 | val ctx = new FrameListenerLoopContext
230 |
231 | def close(): Unit = {
232 | ctx.closed = true
233 | fl.onClose()
234 | }
235 |
236 | val requestAnimation = JsUtils.getOptional[js.Function1[js.Function, Unit]](g.window, "requestAnimationFrame", "webkitRequestAnimationFrame", "mozRequestAnimationFrame", "msRequestAnimationFrame", "oRequestAnimationFrame")
237 | .getOrElse(((fun: js.Function) => {
238 | g.setTimeout(fun, 1000.0 / 60.0)
239 | ()
240 | }): js.Function1[js.Function, Unit])
241 |
242 | def loop(): Unit = {
243 | if (!ctx.closed) {
244 | try {
245 | // Main loop call
246 | val currentTime = JsUtils.now()
247 | val diff = ((currentTime - ctx.lastLoopTime) / 1e3).toFloat
248 | ctx.lastLoopTime = currentTime
249 | val frameEvent = FrameEvent(diff)
250 | val continue = fl.onDraw(frameEvent)
251 | if (continue) {
252 | requestAnimation(loop _)
253 | } else {
254 | close()
255 | }
256 | } catch {
257 | case t: Throwable =>
258 | Console.err.println("Error during onDraw loop of FrameListener")
259 | t.printStackTrace(Console.err)
260 |
261 | close()
262 | }
263 | }
264 | }
265 |
266 | def loopInit(): Unit = {
267 | val readyFuture = try { fl.onCreate() } catch { case t: Throwable => Future.failed(t) }
268 | val ec = scalajs.concurrent.JSExecutionContext.Implicits.runNow
269 | readyFuture.onSuccess {
270 | case _ =>
271 | loop()
272 | }(ec)
273 | readyFuture.onFailure {
274 | case t => // Don't start the loop in case of failure of the given future
275 | Console.err.println("Could not init FrameListener")
276 | t.printStackTrace(Console.err)
277 |
278 | close()
279 | }(ec)
280 |
281 | }
282 |
283 | // Start listener
284 | requestAnimation(loopInit _)
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/audio/Context.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 | import games.Resource
6 | import games.math.Vector3f
7 | import games.JsUtils
8 | import games.Utils
9 |
10 | import java.nio.{ ByteBuffer, ByteOrder }
11 |
12 | import scala.collection.mutable.Set
13 | import scala.concurrent.{ Promise, Future }
14 | import scalajs.concurrent.JSExecutionContext.Implicits.queue
15 |
16 | import scala.collection.{ mutable, immutable }
17 |
18 | import js.Dynamic.{ global => g }
19 |
20 | private[games] object AuroraHelper {
21 | def createDataFromAurora(ctx: WebAudioContext, arraybuffer: js.typedarray.ArrayBuffer): scala.concurrent.Future[JsBufferData] = {
22 | val promise = Promise[JsBufferData]
23 |
24 | val asset = js.Dynamic.global.AV.Asset.fromBuffer(arraybuffer)
25 | asset.on("error", (error: String) => {
26 | promise.failure(new RuntimeException("Aurora returned error: " + error))
27 | })
28 |
29 | asset.decodeToBuffer((data: js.typedarray.Float32Array) => {
30 | val arraybuffer = data.buffer
31 | val byteBuffer = js.typedarray.TypedArrayBuffer.wrap(arraybuffer)
32 |
33 | var optFormat: Option[js.Dynamic] = None
34 | asset.get("format", (format: js.Dynamic) => {
35 | optFormat = Some(format)
36 | })
37 |
38 | optFormat match {
39 | case Some(format) =>
40 | val channels = format.channelsPerFrame.asInstanceOf[Int]
41 | val sampleRate = format.sampleRate.asInstanceOf[Int]
42 | val dataFuture = ctx.prepareRawData(byteBuffer, Format.Float32, channels, sampleRate)
43 | dataFuture.onSuccess { case data => promise.success(data) }
44 | dataFuture.onFailure { case t => promise.failure(new RuntimeException("Aurora decoded successfully, but could not create the Web Audio buffer", t)) }
45 |
46 | case None =>
47 | promise.failure(new RuntimeException("Decoding done, but failed to retrieve the format from Aurora"))
48 | }
49 | })
50 |
51 | promise.future
52 | }
53 |
54 | def createDataFromAurora(ctx: WebAudioContext, res: Resource): scala.concurrent.Future[JsBufferData] = {
55 | Utils.getBinaryDataFromResource(res).flatMap { bb =>
56 | import scala.scalajs.js.typedarray.TypedArrayBufferOps._
57 |
58 | val arrayBuffer = bb.arrayBuffer()
59 | this.createDataFromAurora(ctx, arrayBuffer)
60 | }
61 | }
62 | }
63 |
64 | object WebAudioContext {
65 | lazy val auroraPresent: Boolean = {
66 | JsUtils.getOptional[js.Dynamic](js.Dynamic.global, "AV").flatMap { av => JsUtils.getOptional[js.Dynamic](av, "Asset") }.isDefined
67 | }
68 |
69 | def canUseAurora: Boolean = JsUtils.useAuroraJs && auroraPresent
70 | }
71 |
72 | class WebAudioContext extends Context {
73 | private val audioContext: js.Dynamic = JsUtils.getOptional[js.Dynamic](g, "AudioContext", "webkitAudioContext").getOrElse(throw new RuntimeException("Web Audio API not supported by your browser"))
74 | private[games] val webApi = js.Dynamic.newInstance(audioContext)()
75 |
76 | private lazy val fakeSource = this.createSource()
77 |
78 | private[games] val mainOutput = {
79 | val node = webApi.createGain()
80 | node.connect(webApi.destination)
81 | node.gain.value = 1.0
82 | node
83 | }
84 |
85 | def prepareStreamingData(res: Resource): Future[games.audio.Data] = {
86 | // Streaming data is not a good idea on Android Chrome: https://code.google.com/p/chromium/issues/detail?id=138132#c6
87 | if (JsUtils.Browser.chrome && JsUtils.Browser.android) {
88 | Console.err.println("Warning: Android Chrome does not support streaming data (resource " + res + "), switching to buffered data")
89 | this.prepareBufferedData(res)
90 | } else {
91 | val promise = Promise[games.audio.Data]
92 |
93 | val data = new JsStreamingData(this, res)
94 |
95 | // Try to create a player (to make sure it works)
96 | val playerFuture = data.attach(fakeSource)
97 | playerFuture.onSuccess {
98 | case player =>
99 | player.close()
100 | promise.success(data)
101 | }
102 | playerFuture.onFailure {
103 | case t =>
104 | data.close()
105 | promise.failure(t)
106 | }
107 |
108 | promise.future
109 | }
110 | }
111 | def prepareBufferedData(res: Resource): Future[games.audio.JsBufferData] = {
112 | val dataFuture = Utils.getBinaryDataFromResource(res)
113 | val promise = Promise[JsBufferData]
114 |
115 | dataFuture.onSuccess {
116 | case bb =>
117 | import scala.scalajs.js.typedarray.TypedArrayBufferOps._
118 |
119 | val arraybuffer = bb.arrayBuffer()
120 | this.webApi.decodeAudioData(arraybuffer,
121 | (decodedBuffer: js.Dynamic) => {
122 | promise.success(new JsBufferData(this, decodedBuffer))
123 | },
124 | () => {
125 | val msg = "Failed to decode the audio data from resource " + res
126 | // If Aurora is available and this error seems due to decoding, try with Aurora
127 | if (WebAudioContext.canUseAurora) {
128 | val auroraDataFuture = AuroraHelper.createDataFromAurora(this, arraybuffer)
129 | auroraDataFuture.onSuccess { case auroraData => promise.success(auroraData) }
130 | auroraDataFuture.onFailure { case t => promise.failure(new RuntimeException(msg + " (result with Aurora: " + t + ")", t)) }
131 | } else {
132 | promise.failure(new RuntimeException(msg))
133 | }
134 | })
135 | }
136 | dataFuture.onFailure {
137 | case t =>
138 | promise.failure(t)
139 | }
140 |
141 | promise.future
142 | }
143 | def prepareRawData(data: ByteBuffer, format: Format, channels: Int, freq: Int): Future[games.audio.JsBufferData] = Future {
144 | format match {
145 | case Format.Float32 => // good to go
146 | case _ => throw new RuntimeException("Unsupported data format: " + format)
147 | }
148 |
149 | channels match {
150 | case 1 => // good to go
151 | case 2 => // good to go
152 | case _ => throw new RuntimeException("Unsupported channels number: " + channels)
153 | }
154 |
155 | val floatBuffer = data.slice().order(ByteOrder.nativeOrder()).asFloatBuffer()
156 |
157 | val sampleCount = floatBuffer.remaining() / channels
158 |
159 | val buffer = this.webApi.createBuffer(channels, sampleCount, freq)
160 |
161 | var channelsData = new Array[js.typedarray.Float32Array](channels)
162 |
163 | for (channelCur <- 0 until channels) {
164 | channelsData(channelCur) = buffer.getChannelData(channelCur).asInstanceOf[js.typedarray.Float32Array]
165 | }
166 |
167 | for (sampleCur <- 0 until sampleCount) {
168 | for (channelCur <- 0 until channels) {
169 | channelsData(channelCur)(sampleCur) = floatBuffer.get()
170 | }
171 | }
172 |
173 | new JsBufferData(this, buffer)
174 | }
175 |
176 | def createSource(): JsSource = new JsSource(this, mainOutput)
177 | def createSource3D(): JsSource3D = new JsSource3D(this, mainOutput)
178 |
179 | val listener: JsListener = new JsListener(this)
180 |
181 | def volume: Float = mainOutput.gain.value.asInstanceOf[Double].toFloat
182 | def volume_=(volume: Float) = mainOutput.gain.value = volume.toDouble
183 |
184 | override def close(): Unit = {
185 | super.close()
186 |
187 | mainOutput.disconnect()
188 | }
189 | }
190 |
191 | class JsListener private[games] (ctx: WebAudioContext) extends Listener {
192 | private val orientationData = new Vector3f(0, 0, -1)
193 | private val upData = new Vector3f(0, 1, 0)
194 | private val positionData = new Vector3f(0, 0, 0)
195 |
196 | // Init
197 | ctx.webApi.listener.setPosition(positionData.x.toDouble, positionData.y.toDouble, positionData.z.toDouble)
198 | ctx.webApi.listener.setOrientation(orientationData.x.toDouble, orientationData.y.toDouble, orientationData.z.toDouble, upData.x.toDouble, upData.y.toDouble, upData.z.toDouble)
199 |
200 | def orientation: Vector3f = orientationData.copy()
201 | def position: Vector3f = positionData.copy()
202 | def position_=(position: Vector3f): Unit = {
203 | Vector3f.set(position, positionData)
204 | ctx.webApi.listener.setPosition(positionData.x.toDouble, positionData.y.toDouble, positionData.z.toDouble)
205 | }
206 | def up: Vector3f = upData.copy()
207 | def setOrientation(orientation: Vector3f, up: Vector3f): Unit = {
208 | Vector3f.set(orientation, orientationData)
209 | Vector3f.set(up, upData)
210 | ctx.webApi.listener.setOrientation(orientationData.x.toDouble, orientationData.y.toDouble, orientationData.z.toDouble, upData.x.toDouble, upData.y.toDouble, upData.z.toDouble)
211 | }
212 | }
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/audio/Data.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 | import scala.concurrent.{ Future, Promise }
6 | import scalajs.concurrent.JSExecutionContext.Implicits.queue
7 |
8 | import scala.collection.{ mutable, immutable }
9 |
10 | import games.Resource
11 | import games.Utils
12 | import games.JsUtils
13 | import games.math.Vector3f
14 |
15 | sealed trait JsAbstractSource extends Source {
16 | def inputNode: js.Dynamic
17 |
18 | override def close(): Unit = {
19 | super.close()
20 | }
21 | }
22 | class JsSource(val ctx: WebAudioContext, outputNode: js.Dynamic) extends Source with JsAbstractSource {
23 | val inputNode = outputNode
24 |
25 | ctx.registerSource(this)
26 |
27 | override def close(): Unit = {
28 | super.close()
29 |
30 | ctx.unregisterSource(this)
31 | }
32 | }
33 | class JsSource3D(val ctx: WebAudioContext, outputNode: js.Dynamic) extends Source3D with JsAbstractSource {
34 | val pannerNode = {
35 | val pannerNode = ctx.webApi.createPanner()
36 | pannerNode.connect(outputNode)
37 | pannerNode
38 | }
39 | val inputNode = pannerNode
40 |
41 | private val positionData = new Vector3f(0, 0, 0)
42 |
43 | // Init
44 | this.position = positionData
45 |
46 | ctx.registerSource(this)
47 |
48 | def position: games.math.Vector3f = positionData.copy()
49 | def position_=(position: games.math.Vector3f): Unit = {
50 | Vector3f.set(position, positionData)
51 | pannerNode.setPosition(positionData.x, positionData.y, positionData.z)
52 | }
53 |
54 | override def close(): Unit = {
55 | super.close()
56 |
57 | ctx.unregisterSource(this)
58 |
59 | pannerNode.disconnect()
60 | }
61 | }
62 |
63 | sealed trait JsData extends Data {
64 | override def close(): Unit = {
65 | super.close()
66 | }
67 | }
68 |
69 | class JsBufferData(val ctx: WebAudioContext, webAudioBuffer: js.Dynamic) extends BufferedData with JsData {
70 | ctx.registerData(this)
71 |
72 | def attachNow(source: games.audio.Source): games.audio.JsBufferPlayer = {
73 | val jsSource = source.asInstanceOf[JsAbstractSource]
74 | new JsBufferPlayer(this, jsSource, webAudioBuffer)
75 | }
76 |
77 | override def close(): Unit = {
78 | super.close()
79 |
80 | ctx.unregisterData(this)
81 | }
82 | }
83 |
84 | class JsStreamingData(val ctx: WebAudioContext, res: Resource) extends Data with JsData {
85 | private var backupDataFromAurora: Option[JsBufferData] = None
86 |
87 | ctx.registerData(this)
88 |
89 | def attach(source: games.audio.Source): Future[games.audio.JsPlayer] = {
90 | val promise = Promise[games.audio.JsPlayer]
91 |
92 | val audioElement: js.Dynamic = js.Dynamic.newInstance(js.Dynamic.global.Audio)()
93 | val path = JsUtils.pathForResource(res)
94 | audioElement.src = path
95 |
96 | audioElement.oncanplay = () => {
97 | val jsSource = source.asInstanceOf[JsAbstractSource]
98 | val player = new JsStreamingPlayer(this, jsSource, audioElement)
99 | promise.success(player)
100 | }
101 |
102 | audioElement.onerror = () => {
103 | val errorCode = audioElement.error.code.asInstanceOf[Int]
104 | val errorMessage = errorCode match {
105 | case 1 => "request aborted"
106 | case 2 => "network error"
107 | case 3 => "decoding error"
108 | case 4 => "source not supported"
109 | case _ => "unknown error"
110 | }
111 | val msg = "Failed to load the stream from " + res + ", cause: " + errorMessage
112 |
113 | // If Aurora is available and this error seems due to decoding, try with Aurora
114 | if (WebAudioContext.canUseAurora && (errorCode == 3 || errorCode == 4)) {
115 | backupDataFromAurora match {
116 | case Some(data) => promise.success(data.attachNow(source))
117 | case None =>
118 | val auroraDataFuture = AuroraHelper.createDataFromAurora(ctx, res)
119 | auroraDataFuture.onSuccess {
120 | case data =>
121 | backupDataFromAurora = Some(data)
122 | promise.success(data.attachNow(source))
123 | }
124 | auroraDataFuture.onFailure { case t => promise.failure(new RuntimeException(msg + " (result with Aurora: " + t + ")", t)) }
125 | }
126 |
127 | } else { // TODO is this one really necessary?
128 | if (!promise.isCompleted) promise.failure(new RuntimeException(msg))
129 | else Console.err.println(msg)
130 | }
131 | }
132 |
133 | promise.future
134 | }
135 |
136 | override def close(): Unit = {
137 | super.close()
138 |
139 | ctx.unregisterData(this)
140 |
141 | for (data <- backupDataFromAurora) {
142 | data.close()
143 | }
144 | }
145 | }
146 |
147 | sealed trait JsPlayer extends Player
148 |
149 | class JsBufferPlayer(val data: JsBufferData, val source: JsAbstractSource, webAudioBuffer: js.Dynamic) extends JsPlayer {
150 | // Init
151 | private var sourceNode = data.ctx.webApi.createBufferSource()
152 | sourceNode.buffer = webAudioBuffer
153 | private val gainNode = data.ctx.webApi.createGain()
154 | gainNode.gain.value = 1.0
155 | sourceNode.connect(gainNode)
156 | gainNode.connect(source.inputNode)
157 |
158 | private var isPlaying = false
159 |
160 | private var needRestarting = false
161 | private var nextStartTime = 0.0
162 | private var lastStartDate = 0.0
163 |
164 | source.registerPlayer(this)
165 | data.registerPlayer(this)
166 |
167 | def playing: Boolean = isPlaying
168 | def playing_=(playing: Boolean): Unit = if (playing) {
169 | if (needRestarting) { // a SourceNode can only be started once, need to create a new one
170 | val oldNode = sourceNode
171 | oldNode.disconnect() // disconnect the old node
172 |
173 | sourceNode = data.ctx.webApi.createBufferSource()
174 | sourceNode.loop = oldNode.loop
175 | sourceNode.buffer = oldNode.buffer
176 | sourceNode.playbackRate.value = oldNode.playbackRate.value
177 | sourceNode.connect(gainNode)
178 | }
179 |
180 | sourceNode.start(0, nextStartTime)
181 | lastStartDate = JsUtils.now()
182 | isPlaying = true
183 |
184 | sourceNode.onended = () => {
185 | isPlaying = false
186 | needRestarting = true
187 | nextStartTime = (JsUtils.now() - lastStartDate) / 1000.0 // msec -> sec
188 | }
189 | } else {
190 | sourceNode.stop()
191 | }
192 |
193 | def volume: Float = gainNode.gain.value.asInstanceOf[Double].toFloat
194 | def volume_=(volume: Float) = {
195 | gainNode.gain.value = volume.toDouble
196 | }
197 |
198 | def loop: Boolean = sourceNode.loop.asInstanceOf[Boolean]
199 | def loop_=(loop: Boolean) = {
200 | sourceNode.loop = loop
201 | }
202 |
203 | def pitch: Float = sourceNode.playbackRate.value.asInstanceOf[Double].toFloat
204 | def pitch_=(pitch: Float) = {
205 | sourceNode.playbackRate.value = pitch.toDouble
206 | }
207 |
208 | override def close(): Unit = {
209 | super.close()
210 |
211 | source.unregisterPlayer(this)
212 | data.unregisterPlayer(this)
213 |
214 | sourceNode.disconnect()
215 | gainNode.disconnect()
216 | }
217 | }
218 |
219 | class JsStreamingPlayer(val data: JsStreamingData, val source: JsAbstractSource, audioElement: js.Dynamic) extends JsPlayer {
220 | private val sourceNode = data.ctx.webApi.createMediaElementSource(audioElement)
221 | sourceNode.connect(source.inputNode)
222 |
223 | audioElement.onpause = audioElement.onended = () => {
224 | isPlaying = false
225 | }
226 |
227 | private var isPlaying = false
228 |
229 | source.registerPlayer(this)
230 | data.registerPlayer(this)
231 |
232 | def playing: Boolean = isPlaying
233 | def playing_=(playing: Boolean): Unit = if (playing) {
234 | audioElement.play()
235 | isPlaying = true
236 | } else {
237 | audioElement.pause()
238 | }
239 |
240 | def volume: Float = audioElement.volume.asInstanceOf[Double].toFloat
241 | def volume_=(volume: Float) = {
242 | audioElement.volume = volume.toDouble
243 | }
244 |
245 | def loop: Boolean = audioElement.loop.asInstanceOf[Boolean]
246 | def loop_=(loop: Boolean) = {
247 | audioElement.loop = loop
248 | }
249 |
250 | def pitch: Float = audioElement.playbackRate.asInstanceOf[Double].toFloat
251 | def pitch_=(pitch: Float) = {
252 | audioElement.playbackRate = pitch.toDouble
253 | }
254 |
255 | override def close(): Unit = {
256 | super.close()
257 |
258 | source.unregisterPlayer(this)
259 | data.unregisterPlayer(this)
260 |
261 | sourceNode.disconnect()
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/demo/Specifics.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | object Specifics {
4 | type WebSocketClient = transport.javascript.WebSocketClient
5 | val platformName = "JS"
6 | }
7 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/demoJS/Launcher.scala:
--------------------------------------------------------------------------------
1 | package games.demoJS
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 |
6 | import games._
7 | import games.math
8 | import games.math.Vector3f
9 | import games.opengl._
10 | import games.audio._
11 | import games.input._
12 |
13 | import games.demo._
14 |
15 | import scalajs.concurrent.JSExecutionContext.Implicits.queue
16 |
17 | object Launcher extends js.JSApp {
18 | def main(): Unit = {
19 | JsUtils.setResourcePath("/resources")
20 | JsUtils.orientationLockOnFullscreen = false
21 | if (WebAudioContext.canUseAurora) Console.println("Aurora.js available as fallback")
22 |
23 | val canvas = dom.document.getElementById("demo-canvas-main").asInstanceOf[dom.html.Canvas]
24 |
25 | val itf = new EngineInterface {
26 | def initGL(): GLES2 = new GLES2WebGL(canvas)
27 | def initAudio(): Context = new WebAudioContext()
28 | def initKeyboard(): Keyboard = new KeyboardJS()
29 | def initMouse(): Mouse = new MouseJS(canvas)
30 | def initTouch(): Option[Touchscreen] = Some(new TouchscreenJS(canvas))
31 | def initAccelerometer: Option[Accelerometer] = Some(new AccelerometerJS())
32 | def continue(): Boolean = true
33 | }
34 |
35 | val engine = new Engine(itf)
36 |
37 | Utils.startFrameListener(engine)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/input/Accelerometer.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 |
6 | import scala.concurrent.{ Future, Promise }
7 |
8 | import games.JsUtils
9 | import games.math.Vector3f
10 |
11 | object AccelerometerJS {
12 | private def window = js.Dynamic.global.window
13 | private def screen = js.Dynamic.global.screen
14 |
15 | private var isOrientationLocked: Boolean = false
16 |
17 | private lazy val usingScreenOrientationItf: Boolean = JsUtils.typeName(screen.orientation) == "ScreenOrientation"
18 |
19 | def lockOrientation(orientation: String): Future[Unit] = {
20 | val screen = this.screen
21 |
22 | if (usingScreenOrientationItf) {
23 | val promise = screen.orientation.lock(orientation)
24 | val ret = Promise[Unit]
25 | promise.`then`(() => {
26 | ret.success((): Unit)
27 | isOrientationLocked = true
28 | })
29 | promise.`catch`(() => {
30 | ret.failure(new RuntimeException("Orientation Lock failed"))
31 | })
32 | ret.future
33 | } else {
34 | val lockFun = JsUtils.getOptional[js.Function](screen, "lockOrientation", "mozLockOrientation", "msLockOrientation")
35 | val lockOrientation = lockFun.getOrElse(JsUtils.featureUnsupportedFunction("Orientation Lock"))
36 | screen.lockOrientation = lockOrientation
37 |
38 | if (screen.lockOrientation(orientation).asInstanceOf[Boolean]) {
39 | isOrientationLocked = true
40 | Future.successful((): Unit)
41 | } else {
42 | Future.failed(new RuntimeException("Orientation Lock failed"))
43 | }
44 | }
45 | }
46 |
47 | def unlockOrientation(): Unit = {
48 | val screen = this.screen
49 |
50 | if (usingScreenOrientationItf) {
51 | screen.orientation.unlock()
52 | isOrientationLocked = false
53 | } else {
54 | val unlockFun = JsUtils.getOptional[js.Function](screen, "unlockOrientation", "mozUnlockOrientation", "msUnlockOrientation")
55 | val unlockOrientation = unlockFun.getOrElse(JsUtils.featureUnsupportedFunction("Orientation Unlock"))
56 | screen.unlockOrientation = unlockOrientation
57 |
58 | val retVal = screen.unlockOrientation()
59 | JsUtils.typeName(retVal) match {
60 | case "Boolean" =>
61 | val boolRetVal = retVal.asInstanceOf[Boolean]
62 | if (boolRetVal) {
63 | isOrientationLocked = false
64 | } else {
65 | Console.err.println("Orientation Unlock failed")
66 | }
67 | case x =>
68 | isOrientationLocked = false // Just assume it went fine...
69 | }
70 | }
71 | }
72 |
73 | def currentOrientation(): String = {
74 | val screen = this.screen
75 |
76 | if (usingScreenOrientationItf) screen.orientation.`type`.asInstanceOf[String]
77 | else JsUtils.getOptional[String](screen, "orientation", "mozOrientation", "msOrientation").getOrElse {
78 | Console.err.println(JsUtils.featureUnsupportedText("Orientation Detection"))
79 | "landscape-primary" // Just return standard orientation if not supported
80 | }
81 | }
82 |
83 | def orientationLocked: Boolean = isOrientationLocked
84 | }
85 |
86 | class AccelerometerJS extends Accelerometer {
87 | private var raw: Option[Vector3f] = None
88 |
89 | private val onDeviceMotion: js.Function = (e: js.Dynamic) => {
90 | val acc = e.accelerationIncludingGravity
91 | // Check it's a valid event (Chrome seems to throw some weird stuff on desktop version)
92 | if (acc.x != null && acc.y != null && acc.z != null) {
93 | if (raw.isEmpty) raw = Some(new Vector3f)
94 | val vec = raw.get
95 | // Correct the data according to right-hand coordinates
96 | vec.x = -acc.x.asInstanceOf[Double].toFloat
97 | vec.y = -acc.y.asInstanceOf[Double].toFloat
98 | vec.z = -acc.z.asInstanceOf[Double].toFloat
99 | }
100 | }
101 |
102 | private val window = js.Dynamic.global.window
103 |
104 | // Init
105 | {
106 | window.addEventListener("devicemotion", onDeviceMotion, true)
107 | }
108 |
109 | override def close(): Unit = {
110 | window.removeEventListener("devicemotion", onDeviceMotion, true)
111 | }
112 |
113 | def current(): Option[games.math.Vector3f] = raw.map { vec =>
114 | // Adapt the data to the current orientation of the screen
115 | val orientation = AccelerometerJS.currentOrientation()
116 | orientation match {
117 | case "portrait-primary" => vec.copy()
118 | case "portrait-secondary" => new Vector3f(-vec.x, -vec.y, vec.z)
119 | case "landscape-primary" => new Vector3f(-vec.y, vec.x, vec.z)
120 | case "landscape-secondary" => new Vector3f(vec.y, -vec.x, vec.z)
121 | case _ => vec.copy()
122 | }
123 | }
124 | }
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/input/Keyboard.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 |
6 | import scala.collection.mutable
7 |
8 | import games.JsUtils
9 |
10 | object KeyboardJS {
11 | val keyCodeMapper = new Keyboard.KeyMapper[Int](
12 | (Key.Space, 32),
13 | (Key.Apostrophe, 219), // Chrome
14 | (Key.Apostrophe, 222), // Firefox
15 | //(Key.Circumflex, 229), // buggy on Chrome, unsupported on Firefox
16 | (Key.Comma, 188),
17 | (Key.Period, 190),
18 | (Key.Minus, 189), // Chrome
19 | (Key.Minus, 173), // Firefox
20 | (Key.Slash, 191), // According to Oryol
21 | (Key.N0, 48),
22 | (Key.N1, 49),
23 | (Key.N2, 50),
24 | (Key.N3, 51),
25 | (Key.N4, 52),
26 | (Key.N5, 53),
27 | (Key.N6, 54),
28 | (Key.N7, 55),
29 | (Key.N8, 56),
30 | (Key.N9, 57),
31 | (Key.SemiColon, 59), // According to Oryol
32 | (Key.Equal, 64), // According to Oryol
33 | (Key.A, 65),
34 | (Key.B, 66),
35 | (Key.C, 67),
36 | (Key.D, 68),
37 | (Key.E, 69),
38 | (Key.F, 70),
39 | (Key.G, 71),
40 | (Key.H, 72),
41 | (Key.I, 73),
42 | (Key.J, 74),
43 | (Key.K, 75),
44 | (Key.L, 76),
45 | (Key.M, 77),
46 | (Key.N, 78),
47 | (Key.O, 79),
48 | (Key.P, 80),
49 | (Key.Q, 81),
50 | (Key.R, 82),
51 | (Key.S, 83),
52 | (Key.T, 84),
53 | (Key.U, 85),
54 | (Key.V, 86),
55 | (Key.W, 87),
56 | (Key.X, 88),
57 | (Key.Y, 89),
58 | (Key.Z, 90),
59 | (Key.BracketLeft, 219), // According to Oryol
60 | (Key.BracketRight, 221), // According to Oryol
61 | (Key.BackSlash, 220), // According to Oryol
62 | (Key.GraveAccent, 192),
63 | (Key.Escape, 27),
64 | (Key.Enter, 13),
65 | (Key.Tab, 9),
66 | (Key.BackSpace, 8),
67 | (Key.Insert, 45),
68 | (Key.Delete, 46),
69 | (Key.Right, 39),
70 | (Key.Left, 37),
71 | (Key.Down, 40),
72 | (Key.Up, 38),
73 | (Key.PageUp, 33),
74 | (Key.PageDown, 34),
75 | (Key.Home, 36),
76 | (Key.End, 35),
77 | (Key.CapsLock, 20),
78 | (Key.ScrollLock, 145),
79 | (Key.NumLock, 144),
80 | //(Key.PrintScreen, 777), // Doesn't reach the browser (both Linux/Windows)
81 | (Key.Pause, 19),
82 | (Key.F1, 112),
83 | (Key.F2, 113),
84 | (Key.F3, 114),
85 | (Key.F4, 115),
86 | (Key.F5, 116),
87 | (Key.F6, 117),
88 | (Key.F7, 118),
89 | (Key.F8, 119),
90 | (Key.F9, 120),
91 | (Key.F10, 121),
92 | (Key.F11, 122),
93 | (Key.F12, 123),
94 | // Unable to test F13 to F25
95 | (Key.Num0, 96),
96 | (Key.Num1, 97),
97 | (Key.Num2, 98),
98 | (Key.Num3, 99),
99 | (Key.Num4, 100),
100 | (Key.Num5, 101),
101 | (Key.Num6, 102),
102 | (Key.Num7, 103),
103 | (Key.Num8, 104),
104 | (Key.Num9, 105),
105 | (Key.NumDecimal, 110),
106 | (Key.NumDivide, 111),
107 | (Key.NumMultiply, 106),
108 | (Key.NumSubstract, 109),
109 | (Key.NumAdd, 107),
110 | //(Key.NumEnter, 13), // Duplicate keyCode with key.Enter
111 | //(Key.NumEqual, 777),
112 | (Key.ShiftLeft, 16), // location=1
113 | (Key.ShiftRight, 16), // location=2
114 | (Key.ControlLeft, 17), // location=1
115 | (Key.ControlRight, 17), // location=2
116 | (Key.AltLeft, 18), // location=1
117 | (Key.AltRight, 18), // location=2 // not able to test
118 | //(Key.AltGrLeft, 225) // no location (reported by firefox?)
119 | (Key.SuperLeft, 91), // location=1 // 224 according to oryol
120 | (Key.SuperRight, 91) // location=2
121 | //(Key.MenuLeft, 777), // not able to test
122 | //(Key.MenuRight, 777) // not able to test
123 | )
124 | }
125 |
126 | class KeyboardJS(element: js.Dynamic) extends Keyboard {
127 | def this(el: dom.html.Element) = this(el.asInstanceOf[js.Dynamic])
128 | def this(doc: dom.html.Document) = this(doc.asInstanceOf[js.Dynamic])
129 | def this() = this(dom.document)
130 |
131 | private val eventQueue: mutable.Queue[KeyboardEvent] = mutable.Queue()
132 | private val downKeys: mutable.Set[Key] = mutable.Set()
133 |
134 | private def selectLocatedKey(leftKey: Key, rightKey: Key, location: Int) = location match {
135 | case 1 => leftKey
136 | case 2 => rightKey
137 | case x => leftKey // just default to the left one
138 | }
139 |
140 | private def locateKeyIfNecessary(key: Key, ev: dom.KeyboardEvent): Key = key match {
141 | case Key.ShiftLeft | Key.ShiftRight => selectLocatedKey(Key.ShiftLeft, Key.ShiftRight, ev.location)
142 | case Key.ControlLeft | Key.ControlRight => selectLocatedKey(Key.ControlLeft, Key.ControlRight, ev.location)
143 | case Key.AltLeft | Key.AltRight => selectLocatedKey(Key.AltLeft, Key.AltRight, ev.location)
144 | case Key.SuperLeft | Key.SuperRight => selectLocatedKey(Key.SuperLeft, Key.SuperRight, ev.location)
145 | case _ => key
146 | }
147 |
148 | private def keyFromEvent(ev: dom.KeyboardEvent): Option[Key] = {
149 | val keyCode = ev.keyCode
150 | KeyboardJS.keyCodeMapper.getForRemote(keyCode) match {
151 | case Some(key) => Some(locateKeyIfNecessary(key, ev))
152 | case None => None // unknown keyCode
153 | }
154 | }
155 |
156 | private def keyDown(key: Key): Unit = {
157 | if (!this.isKeyDown(key)) { // Accept this event only if the key was not yet recognized as "down"
158 | downKeys += key
159 | eventQueue += KeyboardEvent(key, true)
160 | }
161 | }
162 |
163 | private def keyUp(key: Key): Unit = {
164 | if (this.isKeyDown(key)) { // Accept this event only if the key was not yet recognized as "up"
165 | downKeys -= key
166 | eventQueue += KeyboardEvent(key, false)
167 | }
168 | }
169 |
170 | private val onKeyUp: js.Function = (e: dom.Event) => {
171 | e.preventDefault()
172 | JsUtils.flushUserEventTasks()
173 |
174 | val ev = e.asInstanceOf[dom.KeyboardEvent]
175 | keyFromEvent(ev) match {
176 | case Some(key) => keyUp(key)
177 | case None => // unknown key, ignore
178 | }
179 | }
180 | private val onKeyDown: js.Function = (e: dom.Event) => {
181 | e.preventDefault()
182 | JsUtils.flushUserEventTasks()
183 |
184 | val ev = e.asInstanceOf[dom.KeyboardEvent]
185 | keyFromEvent(ev) match {
186 | case Some(key) => keyDown(key)
187 | case None => // unknown key, ignore
188 | }
189 | }
190 |
191 | element.addEventListener("keyup", onKeyUp, true)
192 | element.addEventListener("keydown", onKeyDown, true)
193 |
194 | override def close(): Unit = {
195 | super.close()
196 | element.removeEventListener("keyup", onKeyUp, true)
197 | element.removeEventListener("keydown", onKeyDown, true)
198 | }
199 |
200 | def isKeyDown(key: games.input.Key): Boolean = {
201 | downKeys.contains(key)
202 | }
203 |
204 | def nextEvent(): Option[games.input.KeyboardEvent] = {
205 | if (eventQueue.nonEmpty) Some(eventQueue.dequeue())
206 | else None
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/input/Mouse.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 |
6 | import scala.collection.mutable
7 |
8 | import games.JsUtils
9 |
10 | import scala.concurrent.Future
11 |
12 | object MouseJS {
13 | val mapper = new Mouse.ButtonMapper[Int](
14 | (Button.Left, 0),
15 | (Button.Right, 2),
16 | (Button.Middle, 1))
17 |
18 | private def getForLocal(button: Button): Int = button match {
19 | case Button.Aux(num) => num
20 | case _ => MouseJS.mapper.getForLocal(button) match {
21 | case Some(num) => num
22 | case None => throw new RuntimeException("No known LWJGL code for button " + button)
23 | }
24 | }
25 |
26 | private def getForRemote(eventButton: Int): Button = MouseJS.mapper.getForRemote(eventButton) match {
27 | case Some(button) => button
28 | case None => Button.Aux(eventButton)
29 | }
30 | }
31 |
32 | class MouseJS(element: js.Dynamic) extends Mouse {
33 | def this(el: dom.html.Element) = this(el.asInstanceOf[js.Dynamic])
34 | def this(doc: dom.html.Document) = this(doc.asInstanceOf[js.Dynamic])
35 |
36 | private var mouseInside = false
37 | private var dx, dy = 0
38 | private var x, y = 0
39 |
40 | private val eventQueue: mutable.Queue[MouseEvent] = mutable.Queue()
41 | private val downButtons: mutable.Set[Button] = mutable.Set()
42 |
43 | private var ignoreNextRelativeMove = false
44 | private var lockRequested = false
45 |
46 | private def buttonFromEvent(ev: dom.MouseEvent): Button = {
47 | val eventButton = ev.button.asInstanceOf[Int]
48 | val button = MouseJS.getForRemote(eventButton)
49 | button
50 | }
51 |
52 | private val onMouseUp: js.Function = (e: dom.MouseEvent) => {
53 | e.preventDefault()
54 | JsUtils.flushUserEventTasks()
55 |
56 | val button = buttonFromEvent(e)
57 | if (this.isButtonDown(button)) {
58 | downButtons -= button
59 | eventQueue += ButtonEvent(button, false)
60 | }
61 | }
62 | private val onMouseDown: js.Function = (e: dom.MouseEvent) => {
63 | e.preventDefault()
64 | JsUtils.flushUserEventTasks()
65 |
66 | val button = buttonFromEvent(e)
67 | if (!this.isButtonDown(button)) {
68 | downButtons += button
69 | eventQueue += ButtonEvent(button, true)
70 | }
71 | }
72 | private val onMouseMove: js.Function = (e: dom.MouseEvent) => {
73 | e.preventDefault()
74 | //JsUtils.flushUserEventTasks() // Apparently, not considered as a user gesture
75 |
76 | val ev = e.asInstanceOf[js.Dynamic]
77 |
78 | // Get relative position
79 | val movX = JsUtils.getOptional[Double](ev, "movementX", "webkitMovementX", "mozMovementX")
80 | val movY = JsUtils.getOptional[Double](ev, "movementY", "webkitMovementY", "mozMovementY")
81 |
82 | val mx = movX.getOrElse(0.0).toInt
83 | val my = movY.getOrElse(0.0).toInt
84 |
85 | if (this.ignoreNextRelativeMove) {
86 | this.ignoreNextRelativeMove = false
87 | } else {
88 | dx += mx
89 | dy += my
90 | }
91 |
92 | // Get position on element
93 | val offX = ev.offsetX.asInstanceOf[js.UndefOr[Double]]
94 | val offY = ev.offsetY.asInstanceOf[js.UndefOr[Double]]
95 |
96 | val (posX, posY) = if (offX.isDefined && offY.isDefined) { // For WebKit browsers
97 | (offX.get.toInt, offY.get.toInt)
98 | } else { // For... the others
99 | val (offsetX, offsetY) = JsUtils.offsetOfElement(element)
100 | ((e.pageX - offsetX).toInt, (e.pageY - offsetY).toInt)
101 | }
102 |
103 | x = posX
104 | y = posY
105 | }
106 | private val onMouseOver: js.Function = (e: dom.MouseEvent) => {
107 | e.preventDefault()
108 | //JsUtils.flushUserEventTasks() // Apparently, not considered as a user gesture
109 |
110 | mouseInside = true
111 | }
112 | private val onMouseOut: js.Function = (e: dom.MouseEvent) => {
113 | e.preventDefault()
114 | //JsUtils.flushUserEventTasks() // Apparently, not considered as a user gesture
115 |
116 | mouseInside = false
117 | }
118 | private val onMouseWheel: js.Function = (e: dom.WheelEvent) => {
119 | e.preventDefault()
120 | //JsUtils.flushUserEventTasks() // Apparently, not considered as a user gesture
121 |
122 | val ev = e.asInstanceOf[js.Dynamic]
123 |
124 | val wheelX = ev.wheelDeltaX.asInstanceOf[Int]
125 | val wheelY = ev.wheelDeltaY.asInstanceOf[Int]
126 |
127 | if (wheelY > 0) {
128 | eventQueue += WheelEvent(Wheel.Up)
129 | } else if (wheelY < 0) {
130 | eventQueue += WheelEvent(Wheel.Down)
131 | } else if (wheelX > 0) {
132 | eventQueue += WheelEvent(Wheel.Left)
133 | } else if (wheelX < 0) {
134 | eventQueue += WheelEvent(Wheel.Right)
135 | }
136 | }
137 | private val onFirefoxMouseWheel: js.Function = (e: dom.WheelEvent) => {
138 | e.preventDefault()
139 | //JsUtils.flushUserEventTasks() // Apparently, not considered as a user gesture
140 |
141 | val ev = e.asInstanceOf[js.Dynamic]
142 |
143 | val axis = ev.axis.asInstanceOf[Int]
144 | val details = ev.detail.asInstanceOf[Int]
145 |
146 | axis match {
147 | case 2 => eventQueue += WheelEvent(if (details < 0) Wheel.Up else Wheel.Down) // Vertical
148 | case 1 => eventQueue += WheelEvent(if (details < 0) Wheel.Left else Wheel.Right) // horizontal
149 | case _ => // unknown
150 | }
151 | }
152 |
153 | private val onContextMenu: js.Function = (e: dom.Event) => {
154 | false // disable right-click context-menu
155 | }
156 |
157 | private val onPointerLockChange: js.Function = (e: js.Dynamic) => {
158 | if (JsUtils.autoToggling) this.locked = lockRequested // If the lock state has changed against the wish of the user, change back ASAP
159 | //js.Dynamic.global.console.log("onPointerLockChange", this.locked, e)
160 |
161 | // Chrome seems to move the cursor when changing lock state (causing unwanted movement), let's ignore it if it happens during the next 20ms
162 | this.ignoreNextRelativeMove = true
163 | js.Dynamic.global.setTimeout(() => {
164 | this.ignoreNextRelativeMove = false
165 | }, 20)
166 | }
167 | private val onPointerLockError: js.Function = (e: js.Dynamic) => {
168 | // nothing to do?
169 | js.Dynamic.global.console.log("onPointerLockError", this.locked, e)
170 | }
171 |
172 | private val document = dom.document.asInstanceOf[js.Dynamic]
173 |
174 | // Init
175 | {
176 | element.addEventListener("mouseup", onMouseUp, true)
177 | element.addEventListener("mousedown", onMouseDown, true)
178 | element.oncontextmenu = onContextMenu
179 | element.addEventListener("mousemove", onMouseMove, true)
180 | element.addEventListener("mouseover", onMouseOver, true)
181 | element.addEventListener("mouseout", onMouseOut, true)
182 | element.addEventListener("mousewheel", onMouseWheel, true)
183 | element.addEventListener("DOMMouseScroll", onFirefoxMouseWheel, true) // Firefox
184 |
185 | document.addEventListener("pointerlockchange", onPointerLockChange, true)
186 | document.addEventListener("webkitpointerlockchange", onPointerLockChange, true)
187 | document.addEventListener("mozpointerlockchange", onPointerLockChange, true)
188 |
189 | document.addEventListener("pointerlockerror", onPointerLockError, true)
190 | document.addEventListener("webkitpointerlockerror", onPointerLockError, true)
191 | document.addEventListener("mozpointerlockerror", onPointerLockError, true)
192 | }
193 |
194 | override def close(): Unit = {
195 | super.close()
196 |
197 | element.removeEventListener("mouseup", onMouseUp, true)
198 | element.removeEventListener("mousedown", onMouseDown, true)
199 | element.oncontextmenu = js.undefined
200 | element.removeEventListener("mousemove", onMouseMove, true)
201 | element.removeEventListener("mouseover", onMouseOver, true)
202 | element.removeEventListener("mouseout", onMouseOut, true)
203 | element.removeEventListener("mousewheel", onMouseWheel, true)
204 | element.removeEventListener("DOMMouseScroll", onFirefoxMouseWheel, true) // Firefox
205 |
206 | document.removeEventListener("pointerlockchange", onPointerLockChange, true)
207 | document.removeEventListener("webkitpointerlockchange", onPointerLockChange, true)
208 | document.removeEventListener("mozpointerlockchange", onPointerLockChange, true)
209 |
210 | document.removeEventListener("pointerlockerror", onPointerLockError, true)
211 | document.removeEventListener("webkitpointerlockerror", onPointerLockError, true)
212 | document.removeEventListener("mozpointerlockerror", onPointerLockError, true)
213 | }
214 |
215 | def position: games.input.Position = {
216 | Position(x, y)
217 | }
218 | def deltaMotion: games.input.Position = {
219 | val delta = Position(dx, dy)
220 |
221 | // Reset relative position
222 | dx = 0
223 | dy = 0
224 |
225 | delta
226 | }
227 |
228 | val lockRequest = JsUtils.getOptional[js.Dynamic](element, "requestPointerLock", "webkitRequestPointerLock", "mozRequestPointerLock")
229 | val lockExit = JsUtils.getOptional[js.Dynamic](document, "exitPointerLock", "webkitExitPointerLock", "mozExitPointerLock")
230 |
231 | element.lockRequest = lockRequest.getOrElse(JsUtils.featureUnsupportedFunction("Pointer Lock (Request)"))
232 | document.lockExit = lockExit.getOrElse(JsUtils.featureUnsupportedFunction("Pointer Lock (Exit)"))
233 |
234 | def locked: Boolean = JsUtils.getOptional[js.Dynamic](document, "pointerLockElement", "webkitPointerLockElement", "mozPointerLockElement") match {
235 | case Some(el) => el == element
236 | case None => false
237 | }
238 | def locked_=(locked: Boolean): Unit = {
239 | lockRequested = locked // Remember the choice of the user
240 |
241 | val currentlyLocked = this.locked
242 |
243 | if (locked && !currentlyLocked) {
244 | Future {
245 | element.lockRequest()
246 | }(JsUtils.userEventExecutionContext)
247 | } else if (!locked && currentlyLocked) {
248 | Future {
249 | document.lockExit()
250 | }(JsUtils.userEventExecutionContext)
251 | }
252 | }
253 |
254 | def isButtonDown(button: games.input.Button): Boolean = {
255 | downButtons.contains(button)
256 | }
257 | def nextEvent(): Option[games.input.MouseEvent] = {
258 | if (eventQueue.nonEmpty) Some(eventQueue.dequeue())
259 | else None
260 | }
261 |
262 | def isInside(): Boolean = mouseInside
263 | }
264 |
--------------------------------------------------------------------------------
/demo/js/src/main/scala/games/input/Touch.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import scala.scalajs.js
4 | import org.scalajs.dom
5 |
6 | import scala.collection.mutable
7 | import scala.collection.immutable
8 |
9 | import games.JsUtils
10 |
11 | class TouchscreenJS(element: js.Dynamic) extends Touchscreen {
12 | def this(el: dom.html.Element) = this(el.asInstanceOf[js.Dynamic])
13 | def this(doc: dom.html.Document) = this(doc.asInstanceOf[js.Dynamic])
14 |
15 | private val eventQueue: mutable.Queue[TouchEvent] = mutable.Queue()
16 | private val touchsMap: mutable.Map[Int, Touch] = mutable.Map()
17 |
18 | private var nextId: Int = 0
19 |
20 | private val onTouchStart: js.Function = (e: dom.TouchEvent) => {
21 | if (preventMouse) e.preventDefault()
22 | JsUtils.flushUserEventTasks()
23 |
24 | val list = e.changedTouches
25 | for (i <- 0 until list.length) {
26 | val touchJs = list(i)
27 | val prvId = touchJs.identifier
28 | val pubId = nextId
29 | nextId += 1
30 |
31 | // Is there an offsetX/offsetY for such element?
32 | val (offsetX, offsetY) = JsUtils.offsetOfElement(element)
33 | val pos = Position((touchJs.pageX - offsetX).toInt, (touchJs.pageY - offsetY).toInt)
34 | val data = Touch(pubId, pos)
35 | touchsMap += (prvId -> data)
36 | eventQueue += TouchEvent(data, true)
37 | }
38 | }
39 | private val onTouchEnd: js.Function = (e: dom.TouchEvent) => {
40 | if (preventMouse) e.preventDefault()
41 | JsUtils.flushUserEventTasks()
42 |
43 | val list = e.changedTouches
44 | for (i <- 0 until list.length) {
45 | val touchJs = list(i)
46 | val prvId = touchJs.identifier
47 | val pubId = touchsMap(prvId).identifier
48 |
49 | // Is there an offsetX/offsetY for such element?
50 | val (offsetX, offsetY) = JsUtils.offsetOfElement(element)
51 | val pos = Position((touchJs.pageX - offsetX).toInt, (touchJs.pageY - offsetY).toInt)
52 | val data = Touch(pubId, pos)
53 | touchsMap -= prvId
54 | eventQueue += TouchEvent(data, false)
55 | }
56 | }
57 | private val onTouchMove: js.Function = (e: dom.TouchEvent) => {
58 | if (preventMouse) e.preventDefault()
59 | JsUtils.flushUserEventTasks()
60 |
61 | val list = e.changedTouches
62 | for (i <- 0 until list.length) {
63 | val touchJs = list(i)
64 | val prvId = touchJs.identifier
65 | val pubId = touchsMap(prvId).identifier
66 |
67 | // Is there an offsetX/offsetY for such element?
68 | val (offsetX, offsetY) = JsUtils.offsetOfElement(element)
69 | val pos = Position((touchJs.pageX - offsetX).toInt, (touchJs.pageY - offsetY).toInt)
70 | val data = Touch(pubId, pos)
71 | touchsMap += (prvId -> data)
72 | }
73 | }
74 |
75 | // Init
76 | {
77 | element.addEventListener("touchstart", onTouchStart, true)
78 | element.addEventListener("touchend", onTouchEnd, true)
79 | element.addEventListener("touchleave", onTouchEnd, true)
80 | element.addEventListener("touchcancel", onTouchEnd, true)
81 | element.addEventListener("touchmove", onTouchMove, true)
82 | }
83 |
84 | override def close(): Unit = {
85 | element.removeEventListener("touchstart", onTouchStart, true)
86 | element.removeEventListener("touchend", onTouchEnd, true)
87 | element.removeEventListener("touchleave", onTouchEnd, true)
88 | element.removeEventListener("touchcancel", onTouchEnd, true)
89 | element.removeEventListener("touchmove", onTouchMove, true)
90 | }
91 |
92 | def nextEvent(): Option[games.input.TouchEvent] = {
93 | if (eventQueue.nonEmpty) Some(eventQueue.dequeue())
94 | else None
95 | }
96 | def touches: Seq[games.input.Touch] = {
97 | touchsMap.values.toSeq
98 | }
99 |
100 | private var preventMouse: Boolean = true
101 | }
102 |
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/Utils.scala:
--------------------------------------------------------------------------------
1 | package games
2 |
3 | import java.util.concurrent.ConcurrentLinkedQueue
4 | import java.io.InputStream
5 | import java.io.ByteArrayOutputStream
6 | import java.nio.{ ByteBuffer, ByteOrder }
7 | import org.lwjgl.opengl._
8 | import java.io.BufferedReader
9 | import java.io.InputStreamReader
10 | import javax.imageio.ImageIO
11 |
12 | import scala.concurrent.{ Await, Future, ExecutionContext }
13 | import scala.concurrent.duration.Duration
14 | import scala.util.{ Success, Failure }
15 |
16 | import games.opengl.GLES2
17 |
18 | private[games] class ExplicitExecutionContext extends ExecutionContext {
19 | private val pendingRunnables = new ConcurrentLinkedQueue[Runnable]
20 |
21 | def execute(runnable: Runnable): Unit = {
22 | pendingRunnables.add(runnable)
23 | }
24 | def reportFailure(cause: Throwable): Unit = {
25 | ExecutionContext.defaultReporter(cause)
26 | }
27 |
28 | /**
29 | * Flush all the currently pending runnables.
30 | * You don't need to explicitly use this method if you use the FrameListener loop system.
31 | * Warning: this should be called only from the OpenGL thread!
32 | */
33 | def flushPending(): Unit = {
34 | var current: Runnable = null
35 | while ({ current = pendingRunnables.poll(); current } != null) {
36 | try { current.run() }
37 | catch { case t: Throwable => this.reportFailure(t) }
38 | }
39 | }
40 | }
41 |
42 | object JvmUtils {
43 | private[games] def streamForResource(res: Resource): InputStream = {
44 | val stream = JvmUtils.getClass().getResourceAsStream(res.name)
45 | if (stream == null) throw new RuntimeException("Could not retrieve resource " + res.name)
46 | stream
47 | }
48 | }
49 |
50 | trait UtilsImpl extends UtilsRequirements {
51 | private[games] def getLoopThreadExecutionContext(): ExecutionContext = new ExplicitExecutionContext
52 |
53 | def getBinaryDataFromResource(res: games.Resource)(implicit ec: ExecutionContext): scala.concurrent.Future[java.nio.ByteBuffer] = {
54 | Future {
55 | val stream = JvmUtils.streamForResource(res)
56 | val byteStream = new ByteArrayOutputStream()
57 | val tmpData: Array[Byte] = new Array[Byte](4096) // 4KiB of temp data
58 |
59 | var tmpDataContentSize: Int = 0
60 | while ({ tmpDataContentSize = stream.read(tmpData); tmpDataContentSize } >= 0) {
61 | byteStream.write(tmpData, 0, tmpDataContentSize)
62 | }
63 |
64 | stream.close()
65 |
66 | val byteArray = byteStream.toByteArray()
67 | val byteBuffer = ByteBuffer.allocate(byteArray.length)
68 |
69 | byteBuffer.put(byteArray)
70 | byteBuffer.rewind()
71 |
72 | byteBuffer
73 | }
74 | }
75 | def getTextDataFromResource(res: games.Resource)(implicit ec: ExecutionContext): scala.concurrent.Future[String] = {
76 | Future {
77 | val stream = JvmUtils.streamForResource(res)
78 | val streamReader = new InputStreamReader(stream)
79 | val reader = new BufferedReader(streamReader)
80 |
81 | val text = new StringBuilder()
82 |
83 | val buffer = Array[Char](4096) // 4KiB buffer
84 | var bufferReadLength = 0
85 |
86 | while ({ bufferReadLength = reader.read(buffer); bufferReadLength } >= 0) {
87 | text.appendAll(buffer, 0, bufferReadLength)
88 | }
89 |
90 | reader.close()
91 | streamReader.close()
92 | stream.close()
93 |
94 | text.toString()
95 | }
96 | }
97 | def loadTexture2DFromResource(res: games.Resource, texture: games.opengl.Token.Texture, gl: games.opengl.GLES2, openglExecutionContext: ExecutionContext)(implicit ec: ExecutionContext): scala.concurrent.Future[Unit] = {
98 | Future {
99 | val stream = JvmUtils.streamForResource(res)
100 |
101 | // Should support JPEG, PNG, BMP, WBMP and GIF
102 | val image = ImageIO.read(stream)
103 |
104 | val height = image.getHeight()
105 | val width = image.getWidth()
106 | val byteBuffer = GLES2.createByteBuffer(4 * width * height) // Stored as RGBA value: 4 bytes per pixel
107 | val intBuffer = byteBuffer.duplicate().order(ByteOrder.BIG_ENDIAN).asIntBuffer()
108 | val tmp = new Array[Byte](4)
109 | for (y <- 0 until height) {
110 | for (x <- 0 until width) {
111 | val argb = image.getRGB(x, y)
112 | intBuffer.put((argb >>> 24) | (argb << 8))
113 | }
114 | }
115 | stream.close()
116 |
117 | (width, height, byteBuffer)
118 | }.map { // Execute this part with the openglExecutionContext instead of the standard one
119 | case (width, height, byteBuffer) =>
120 | val previousTexture = gl.getParameterTexture(GLES2.TEXTURE_BINDING_2D)
121 | gl.bindTexture(GLES2.TEXTURE_2D, texture)
122 | gl.texImage2D(GLES2.TEXTURE_2D, 0, GLES2.RGBA, width, height, 0, GLES2.RGBA, GLES2.UNSIGNED_BYTE, byteBuffer)
123 | gl.bindTexture(GLES2.TEXTURE_2D, previousTexture)
124 | }(openglExecutionContext)
125 | }
126 | def startFrameListener(fl: games.FrameListener): Unit = {
127 | def executePending(): Unit = fl.loopExecutionContext.asInstanceOf[ExplicitExecutionContext].flushPending()
128 |
129 | val frameListenerThread = new Thread(new Runnable {
130 | def run() {
131 | var lastLoopTime: Long = System.nanoTime()
132 | val readyFuture = try { fl.onCreate() } catch { case t: Throwable => Future.failed(t) }
133 |
134 | while (!readyFuture.isCompleted) {
135 | // Execute the pending tasks
136 | executePending()
137 | Thread.sleep(100) // Don't exhaust the CPU, 10Hz should be enough
138 | }
139 |
140 | var continue = readyFuture.value.get match {
141 | case Success(_) => // Ok, nothing to do, just continue
142 | true
143 | case Failure(t) =>
144 | Console.err.println("Could not init FrameListener")
145 | t.printStackTrace(Console.err)
146 | false
147 | }
148 |
149 | try while (continue) {
150 | // Execute the pending tasks
151 | executePending()
152 |
153 | // Main loop call
154 | val currentTime: Long = System.nanoTime()
155 | val diff = ((currentTime - lastLoopTime) / 1e9).toFloat
156 | lastLoopTime = currentTime
157 | val frameEvent = FrameEvent(diff)
158 | continue = continue && fl.onDraw(frameEvent)
159 |
160 | Display.update()
161 | } catch {
162 | case t: Throwable =>
163 | Console.err.println("Error during onDraw loop of FrameListener")
164 | t.printStackTrace(Console.err)
165 | }
166 |
167 | executePending
168 | fl.onClose()
169 | executePending
170 | }
171 | })
172 | // Start listener
173 | frameListenerThread.start()
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/audio/Context.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import org.lwjgl.openal.AL
4 | import org.lwjgl.openal.AL10
5 | import org.lwjgl.openal.Util
6 |
7 | import games.math.Vector3f
8 | import games.JvmUtils
9 |
10 | import java.nio.ByteBuffer
11 | import java.nio.ByteOrder
12 | import java.io.ByteArrayOutputStream
13 | import java.io.EOFException
14 |
15 | import scala.concurrent.{ Promise, Future }
16 | import scala.concurrent.ExecutionContext.Implicits.global
17 | import scala.collection.{ mutable, immutable }
18 |
19 | class ALContext extends Context {
20 | private val streamingThreads: mutable.Set[Thread] = mutable.Set()
21 | private val lock = new Object()
22 | private[games] def registerStreamingThread(thread: Thread): Unit = lock.synchronized { streamingThreads += thread }
23 | private[games] def unregisterStreamingThread(thread: Thread): Unit = lock.synchronized {
24 | streamingThreads -= thread
25 | lock.notifyAll()
26 | }
27 | private[games] def waitForStreamingThreads(): Unit = lock.synchronized {
28 | while (!streamingThreads.isEmpty) lock.wait()
29 | }
30 |
31 | private lazy val fakeSource = this.createSource()
32 |
33 | AL.create()
34 |
35 | def prepareBufferedData(res: games.Resource): scala.concurrent.Future[games.audio.BufferedData] = Future {
36 | val alBuffer = AL10.alGenBuffers()
37 | var decoder: VorbisDecoder = null
38 | try {
39 | val in = JvmUtils.streamForResource(res)
40 | decoder = new VorbisDecoder(in, FixedSigned16Converter)
41 | val transfertBufferSize = 4096
42 | val transfertBuffer = ByteBuffer.allocate(transfertBufferSize).order(ByteOrder.nativeOrder())
43 | val dataStream = new ByteArrayOutputStream(transfertBufferSize)
44 |
45 | var totalDataLength = 0
46 |
47 | try {
48 | while (true) {
49 | transfertBuffer.rewind()
50 | val dataLength = decoder.read(transfertBuffer)
51 | val data = transfertBuffer.array()
52 | dataStream.write(data, 0, dataLength)
53 | totalDataLength += dataLength
54 | }
55 | } catch {
56 | case e: EOFException => // end of stream reached, exit loop
57 | }
58 |
59 | val dataArray = dataStream.toByteArray()
60 | require(totalDataLength == dataArray.length) // TODO remove later, sanity check
61 | val dataBuffer = ByteBuffer.allocateDirect(dataArray.length).order(ByteOrder.nativeOrder())
62 |
63 | dataBuffer.put(dataArray)
64 | dataBuffer.rewind()
65 |
66 | val format = decoder.channels match {
67 | case 1 => AL10.AL_FORMAT_MONO16
68 | case 2 => AL10.AL_FORMAT_STEREO16
69 | case x => throw new RuntimeException("Only mono or stereo data are supported. Found channels: " + x)
70 | }
71 |
72 | AL10.alBufferData(alBuffer, format, dataBuffer, decoder.rate)
73 |
74 | decoder.close()
75 | decoder = null
76 |
77 | val ret = new ALBufferData(this, alBuffer)
78 | Util.checkALError()
79 | ret
80 | } catch {
81 | case t: Throwable =>
82 | if (decoder != null) {
83 | decoder.close()
84 | decoder = null
85 | }
86 | AL10.alDeleteBuffers(alBuffer)
87 | Util.checkALError()
88 | throw t
89 | }
90 | }
91 | def prepareRawData(data: java.nio.ByteBuffer, format: games.audio.Format, channels: Int, freq: Int): scala.concurrent.Future[games.audio.BufferedData] = Future {
92 | val alBuffer = AL10.alGenBuffers()
93 | try {
94 | format match {
95 | case Format.Float32 => // good to go
96 | case _ => throw new RuntimeException("Unsupported data format: " + format)
97 | }
98 |
99 | val channelFormat = channels match {
100 | case 1 => AL10.AL_FORMAT_MONO16
101 | case 2 => AL10.AL_FORMAT_STEREO16
102 | case _ => throw new RuntimeException("Unsupported channels number: " + channels)
103 | }
104 |
105 | val converter = FixedSigned16Converter
106 | val fb = data.slice().order(ByteOrder.nativeOrder()).asFloatBuffer()
107 |
108 | val sampleCount = fb.remaining() / channels
109 |
110 | val openalData = ByteBuffer.allocateDirect(2 * channels * sampleCount).order(ByteOrder.nativeOrder())
111 |
112 | for (sampleCur <- 0 until sampleCount) {
113 | for (channelCur <- 0 until channels) {
114 | val value = fb.get()
115 | converter(value, openalData)
116 | }
117 | }
118 |
119 | openalData.rewind()
120 |
121 | AL10.alBufferData(alBuffer, channelFormat, openalData, freq)
122 |
123 | val ret = new ALBufferData(this, alBuffer)
124 | Util.checkALError()
125 | ret
126 | } catch {
127 | case t: Throwable =>
128 | AL10.alDeleteBuffers(alBuffer)
129 | Util.checkALError()
130 | throw t
131 | }
132 | }
133 | def prepareStreamingData(res: games.Resource): scala.concurrent.Future[games.audio.Data] = {
134 | val promise = Promise[games.audio.Data]
135 |
136 | val data = new ALStreamingData(this, res)
137 |
138 | // Try to create a player (to make sure it works)
139 | val playerFuture = data.attach(fakeSource)
140 | playerFuture.onSuccess {
141 | case player =>
142 | player.close()
143 | promise.success(data)
144 | }
145 | playerFuture.onFailure {
146 | case t =>
147 | data.close()
148 | promise.failure(t)
149 | }
150 |
151 | promise.future
152 | }
153 |
154 | def createSource(): games.audio.Source = new ALSource(this)
155 | def createSource3D(): games.audio.Source3D = new ALSource3D(this)
156 |
157 | val listener: games.audio.Listener = new ALListener()
158 |
159 | def volume: Float = masterVolume
160 | def volume_=(volume: Float): Unit = {
161 | masterVolume = volume
162 | for (
163 | source <- sources;
164 | player <- source.players
165 | ) {
166 | val alPlayer = player.asInstanceOf[ALPlayer]
167 | alPlayer.applyChangedVolume()
168 | }
169 | }
170 |
171 | private[games] var masterVolume = 1f
172 |
173 | override def close(): Unit = {
174 | super.close()
175 |
176 | // Wait for all the streaming threads to have done their work
177 | this.waitForStreamingThreads()
178 |
179 | AL.destroy()
180 | }
181 | }
182 |
183 | class ALListener private[games] () extends Listener {
184 | private val orientationBuffer = ByteBuffer.allocateDirect(2 * 3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
185 | private val positionBuffer = ByteBuffer.allocateDirect(1 * 3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
186 |
187 | // Preload buffer
188 | AL10.alGetListener(AL10.AL_POSITION, positionBuffer)
189 | AL10.alGetListener(AL10.AL_ORIENTATION, orientationBuffer)
190 | Util.checkALError()
191 |
192 | def position: Vector3f = {
193 | positionBuffer.rewind()
194 | val ret = new Vector3f
195 | ret.load(positionBuffer)
196 | ret
197 | }
198 | def position_=(position: Vector3f): Unit = {
199 | positionBuffer.rewind()
200 | position.store(positionBuffer)
201 | positionBuffer.rewind()
202 | AL10.alListener(AL10.AL_POSITION, positionBuffer)
203 | }
204 |
205 | def up: Vector3f = {
206 | orientationBuffer.position(3)
207 | val ret = new Vector3f
208 | ret.load(orientationBuffer)
209 | ret
210 | }
211 |
212 | def orientation: Vector3f = {
213 | orientationBuffer.rewind()
214 | val ret = new Vector3f
215 | ret.load(orientationBuffer)
216 | ret
217 | }
218 | def setOrientation(orientation: Vector3f, up: Vector3f): Unit = {
219 | orientationBuffer.rewind()
220 | orientation.store(orientationBuffer)
221 | up.store(orientationBuffer)
222 | orientationBuffer.rewind()
223 | AL10.alListener(AL10.AL_ORIENTATION, orientationBuffer)
224 | }
225 | }
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/audio/Converter.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import java.nio.ByteBuffer
4 |
5 | trait Converter {
6 | def apply(value: Float, dst: ByteBuffer): Unit
7 | val bytePerValue: Int
8 | }
9 |
10 | object FixedSigned8Converter extends Converter {
11 | def apply(value: Float, dst: ByteBuffer): Unit = {
12 | val amplified = (value * Byte.MaxValue).toInt
13 | val clamped = Math.max(Byte.MinValue, Math.min(Byte.MaxValue, amplified)).toByte
14 | dst.put(clamped)
15 | }
16 | val bytePerValue = 1
17 | }
18 |
19 | /**
20 | * Usable for OpenAL with format AL_FORMAT_MONO8/AL_FORMAT_STEREO8
21 | */
22 | object FixedUnsigned8Converter extends Converter {
23 | private val max = 255
24 |
25 | def apply(value: Float, dst: ByteBuffer): Unit = {
26 | val amplified = ((value + 1) / 2 * max).toInt
27 | val clamped = Math.max(0, Math.min(max, amplified)).toByte
28 | dst.put(clamped)
29 | }
30 | val bytePerValue = 1
31 | }
32 |
33 | /**
34 | * Usable for OpenAL with format AL_FORMAT_MONO16/AL_FORMAT_STEREO16
35 | */
36 | object FixedSigned16Converter extends Converter {
37 | def apply(value: Float, dst: ByteBuffer): Unit = {
38 | val amplified = (value * Short.MaxValue).toInt
39 | val clamped = Math.max(Short.MinValue, Math.min(Short.MaxValue, amplified)).toShort
40 | dst.putShort(clamped)
41 | }
42 | val bytePerValue = 2
43 | }
44 |
45 | object FixedUnsigned16Converter extends Converter {
46 | private val max = 65535
47 |
48 | def apply(value: Float, dst: ByteBuffer): Unit = {
49 | val amplified = ((value + 1) / 2 * max).toInt
50 | val clamped = Math.max(0, Math.min(max, amplified)).toShort
51 | dst.putShort(clamped)
52 | }
53 | val bytePerValue = 2
54 | }
55 |
56 | object Floating32Converter extends Converter {
57 | def apply(value: Float, dst: ByteBuffer): Unit = {
58 | dst.putFloat(value)
59 | }
60 | val bytePerValue = 4
61 | }
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/audio/Data.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import games.Resource
4 | import games.math.Vector3f
5 | import games.JvmUtils
6 |
7 | import scala.concurrent.{ Promise, Future }
8 | import scala.concurrent.ExecutionContext.Implicits.global
9 |
10 | import scala.collection.{ mutable, immutable }
11 |
12 | import java.nio.{ ByteBuffer, ByteOrder }
13 | import java.io.EOFException
14 |
15 | import org.lwjgl.openal.AL10
16 | import org.lwjgl.openal.Util
17 |
18 | sealed trait ALAbstractSource extends Source {
19 | override def close(): Unit = {
20 | super.close()
21 | }
22 | }
23 |
24 | class ALSource(val ctx: ALContext) extends Source with ALAbstractSource {
25 | ctx.registerSource(this)
26 |
27 | override private[games] def registerPlayer(player: Player): Unit = {
28 | super.registerPlayer(player)
29 |
30 | // Apply spatial attributes right now
31 | val alSource = player.asInstanceOf[ALPlayer].alSource
32 | AL10.alSourcei(alSource, AL10.AL_SOURCE_RELATIVE, AL10.AL_TRUE)
33 | AL10.alSource3f(alSource, AL10.AL_POSITION, 0f, 0f, 0f)
34 | AL10.alSource3f(alSource, AL10.AL_VELOCITY, 0f, 0f, 0f)
35 |
36 | Util.checkALError()
37 | }
38 |
39 | override def close(): Unit = {
40 | super.close()
41 |
42 | ctx.unregisterSource(this)
43 | }
44 | }
45 |
46 | class ALSource3D(val ctx: ALContext) extends Source3D with ALAbstractSource {
47 | def position: games.math.Vector3f = {
48 | positionBuffer.rewind()
49 | val ret = new Vector3f
50 | ret.load(positionBuffer)
51 | ret
52 | }
53 | def position_=(position: games.math.Vector3f): Unit = {
54 | positionBuffer.rewind()
55 | position.store(positionBuffer)
56 | positionBuffer.rewind()
57 | for (player <- this.players) {
58 | val alSource = player.asInstanceOf[ALPlayer].alSource
59 | AL10.alSource(alSource, AL10.AL_POSITION, positionBuffer)
60 | }
61 | Util.checkALError()
62 | }
63 |
64 | override private[games] def registerPlayer(player: Player): Unit = {
65 | super.registerPlayer(player)
66 |
67 | // Apply spatial attributes right now
68 | val alSource = player.asInstanceOf[ALPlayer].alSource
69 | AL10.alSource(alSource, AL10.AL_POSITION, positionBuffer)
70 | AL10.alGetSource(alSource, AL10.AL_POSITION, positionBuffer)
71 | Util.checkALError()
72 | }
73 |
74 | private val positionBuffer = ByteBuffer.allocateDirect(3 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer()
75 |
76 | ctx.registerSource(this)
77 |
78 | override def close(): Unit = {
79 | super.close()
80 |
81 | ctx.unregisterSource(this)
82 | }
83 | }
84 |
85 | sealed trait ALData extends Data {
86 | private[games] val ctx: ALContext
87 |
88 | override def close(): Unit = {
89 | super.close()
90 | }
91 | }
92 |
93 | class ALBufferData(val ctx: ALContext, alBuffer: Int) extends BufferedData with ALData {
94 | ctx.registerData(this)
95 |
96 | def attachNow(source: games.audio.Source): games.audio.Player = {
97 | val alSource = AL10.alGenSources()
98 | try {
99 | AL10.alSourcei(alSource, AL10.AL_BUFFER, alBuffer)
100 |
101 | val alAudioSource = source.asInstanceOf[ALAbstractSource]
102 | val ret = new ALBufferPlayer(this, alAudioSource, alSource)
103 | Util.checkALError()
104 | ret
105 | } catch {
106 | case t: Throwable =>
107 | AL10.alDeleteSources(alSource)
108 | Util.checkALError()
109 | throw t
110 | }
111 | }
112 |
113 | override def close(): Unit = {
114 | super.close()
115 |
116 | ctx.unregisterData(this)
117 |
118 | AL10.alDeleteBuffers(alBuffer)
119 | Util.checkALError()
120 | }
121 | }
122 |
123 | class ALStreamingData(val ctx: ALContext, res: Resource) extends Data with ALData {
124 | ctx.registerData(this)
125 |
126 | def attach(source: games.audio.Source): scala.concurrent.Future[games.audio.Player] = {
127 | val promise = Promise[games.audio.Player]
128 |
129 | val alSource = AL10.alGenSources()
130 | val alAudioSource = source.asInstanceOf[ALAbstractSource]
131 | val player = new ALStreamingPlayer(this, alAudioSource, alSource, res)
132 | Util.checkALError()
133 |
134 | player.ready.onSuccess { case _ => promise.success(player) }
135 | player.ready.onFailure {
136 | case t: Throwable =>
137 | promise.failure(t)
138 | Util.checkALError()
139 | }
140 |
141 | promise.future
142 | }
143 |
144 | override def close(): Unit = {
145 | super.close()
146 |
147 | ctx.unregisterData(this)
148 | }
149 | }
150 |
151 | sealed trait ALPlayer extends Player {
152 | private[games] val alSource: Int
153 |
154 | private[games] def applyChangedVolume(): Unit
155 | }
156 |
157 | abstract class ALBasicPlayer(val data: ALData, val source: ALAbstractSource, val alSource: Int) extends ALPlayer {
158 | source.registerPlayer(this)
159 | data.registerPlayer(this)
160 |
161 | private var thisVolume = 1f
162 |
163 | private[games] def applyChangedVolume(): Unit = {
164 | val curVolume = data.ctx.masterVolume * thisVolume
165 | AL10.alSourcef(alSource, AL10.AL_GAIN, curVolume)
166 | Util.checkALError()
167 | }
168 |
169 | // Init
170 | applyChangedVolume()
171 |
172 | def volume: Float = thisVolume
173 | def volume_=(volume: Float): Unit = {
174 | thisVolume = volume
175 | applyChangedVolume
176 | }
177 |
178 | override def close(): Unit = {
179 | super.close()
180 |
181 | source.unregisterPlayer(this)
182 | data.unregisterPlayer(this)
183 | }
184 | }
185 |
186 | class ALBufferPlayer(override val data: ALBufferData, override val source: ALAbstractSource, override val alSource: Int) extends ALBasicPlayer(data, source, alSource) {
187 | def loop: Boolean = {
188 | val ret = AL10.alGetSourcei(alSource, AL10.AL_LOOPING) == AL10.AL_TRUE
189 | Util.checkALError()
190 | ret
191 | }
192 | def loop_=(loop: Boolean): Unit = {
193 | AL10.alSourcei(alSource, AL10.AL_LOOPING, if (loop) AL10.AL_TRUE else AL10.AL_FALSE)
194 | Util.checkALError()
195 | }
196 |
197 | def pitch: Float = {
198 | val ret = AL10.alGetSourcef(alSource, AL10.AL_PITCH)
199 | Util.checkALError()
200 | ret
201 | }
202 | def pitch_=(pitch: Float): Unit = {
203 | AL10.alSourcef(alSource, AL10.AL_PITCH, pitch)
204 | Util.checkALError()
205 | }
206 |
207 | def playing: Boolean = {
208 | val ret = AL10.alGetSourcei(alSource, AL10.AL_SOURCE_STATE) == AL10.AL_PLAYING
209 | Util.checkALError()
210 | ret
211 | }
212 | def playing_=(playing: Boolean): Unit = if (playing) {
213 | AL10.alSourcePlay(alSource)
214 | Util.checkALError()
215 | } else {
216 | AL10.alSourcePause(alSource)
217 | Util.checkALError()
218 | }
219 |
220 | override def close(): Unit = {
221 | super.close()
222 |
223 | AL10.alDeleteSources(alSource)
224 | Util.checkALError()
225 | }
226 | }
227 |
228 | class ALStreamingPlayer(override val data: ALStreamingData, override val source: ALAbstractSource, override val alSource: Int, res: Resource) extends ALBasicPlayer(data, source, alSource) { thisPlayer =>
229 |
230 | private val converter: Converter = FixedSigned16Converter
231 |
232 | private def initStreamingThread() = {
233 | val streamingThread = new Thread() { thisStreamingThread =>
234 | override def run(): Unit = {
235 | data.ctx.registerStreamingThread(thisStreamingThread)
236 | val numBuffers = 8 // buffers
237 | val alBuffers = new Array[Int](numBuffers)
238 | var decoder: VorbisDecoder = null
239 | try {
240 | // Init
241 | decoder = new VorbisDecoder(JvmUtils.streamForResource(res), converter)
242 |
243 | val bufferedTime = 2.0f // amount of time buffered
244 |
245 | val streamingInterval = 1.0f // check for buffer every second
246 |
247 | require(streamingInterval < bufferedTime) // TODO remove later, sanity check, buffer will go empty before we can feed them else (beware of the pitch!)
248 |
249 | val bufferSampleSize = ((bufferedTime * decoder.rate) / numBuffers).toInt
250 | val bufferSize = bufferSampleSize * decoder.channels * converter.bytePerValue
251 |
252 | val tmpBufferData = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder())
253 |
254 | val format = decoder.channels match {
255 | case 1 => AL10.AL_FORMAT_MONO16
256 | case 2 => AL10.AL_FORMAT_STEREO16
257 | case x => throw new RuntimeException("Only mono or stereo data are supported. Found channels: " + x)
258 | }
259 |
260 | for (i <- 0 until alBuffers.length) {
261 | alBuffers(i) = AL10.alGenBuffers()
262 | }
263 | Util.checkALError()
264 |
265 | var buffersReady: List[Int] = alBuffers.toList
266 |
267 | /**
268 | * Fill the buffer with the data from the decoder
269 | * Returns false if the end of the stream has been reached (but the data in the buffer are still valid up to the limit), true else
270 | */
271 | def fillBuffer(buffer: ByteBuffer): Boolean = {
272 | buffer.clear()
273 |
274 | val ret = try {
275 | decoder.readFully(buffer)
276 | true
277 | } catch {
278 | case e: EOFException => false
279 | }
280 |
281 | buffer.flip()
282 |
283 | ret
284 | }
285 |
286 | var running = true
287 | var last = System.currentTimeMillis()
288 |
289 | // Main thread loop
290 | while (threadRunning) {
291 | // if we are using this streaming thread...
292 | if (running) {
293 | // Retrieve the used buffer
294 | val processed = AL10.alGetSourcei(alSource, AL10.AL_BUFFERS_PROCESSED)
295 | for (i <- 0 until processed) {
296 | val alBuffer = AL10.alSourceUnqueueBuffers(alSource)
297 | buffersReady = alBuffer :: buffersReady
298 | }
299 |
300 | // Fill the buffer and send them to OpenAL again
301 | while (running && !buffersReady.isEmpty) {
302 | val alBuffer = buffersReady.head
303 | buffersReady = buffersReady.tail
304 |
305 | running = fillBuffer(tmpBufferData)
306 | AL10.alBufferData(alBuffer, format, tmpBufferData, decoder.rate)
307 | AL10.alSourceQueueBuffers(alSource, alBuffer)
308 | Util.checkALError()
309 |
310 | // Check for looping
311 | if (!running && looping) {
312 | decoder.close()
313 | decoder = new VorbisDecoder(JvmUtils.streamForResource(res), converter)
314 | running = true
315 | }
316 | }
317 |
318 | // We should have enough data to start the playback at this point
319 | if (!promiseReady.isCompleted) promiseReady.success((): Unit)
320 | }
321 |
322 | // Sleep a while, adjust for pitch (playback rate)
323 | try {
324 | val now = System.currentTimeMillis()
325 | val elapsedTime = now - last
326 | last = System.currentTimeMillis()
327 | val remainingTime = streamingInterval - elapsedTime
328 | if (remainingTime > 0) { // Sleep only
329 | val sleepingTime = (remainingTime / pitchCache * 1000).toLong
330 | Thread.sleep(sleepingTime)
331 | }
332 | } catch {
333 | case e: InterruptedException => // just wake up and do your thing
334 | }
335 |
336 | }
337 |
338 | // Closing
339 | decoder.close()
340 | decoder = null
341 | } catch {
342 | case t: Throwable =>
343 | val ex = new RuntimeException("Error in the streaming thread", t)
344 | if (promiseReady.isCompleted) throw ex
345 | else promiseReady.failure(ex)
346 | } finally {
347 | if (decoder != null) {
348 | decoder.close()
349 | decoder = null
350 | }
351 | AL10.alDeleteSources(alSource)
352 | for (alBuffer <- alBuffers) {
353 | AL10.alDeleteBuffers(alBuffer)
354 | }
355 | data.ctx.unregisterStreamingThread(thisStreamingThread)
356 | Util.checkALError()
357 | }
358 | }
359 | }
360 | streamingThread.setDaemon(true)
361 | streamingThread.start()
362 |
363 | streamingThread
364 | }
365 |
366 | private[games] var streamingThread = this.initStreamingThread()
367 |
368 | private def wakeUpThread() {
369 | streamingThread.interrupt()
370 | }
371 |
372 | private var threadRunning = true
373 | private val promiseReady = Promise[Unit]
374 | private[games] val ready = promiseReady.future
375 |
376 | private var looping = false
377 | private var pitchCache = 1f
378 |
379 | def loop: Boolean = looping
380 | def loop_=(loop: Boolean): Unit = looping = loop
381 |
382 | def pitch: Float = {
383 | val ret = if (AL10.alIsSource(alSource)) AL10.alGetSourcef(alSource, AL10.AL_PITCH) else pitchCache
384 | Util.checkALError()
385 | ret
386 | }
387 | def pitch_=(pitch: Float): Unit = {
388 | pitchCache = pitch
389 | if (AL10.alIsSource(alSource)) AL10.alSourcef(alSource, AL10.AL_PITCH, pitch)
390 | Util.checkALError()
391 | wakeUpThread() // so it can adjust to the new playback rate
392 | }
393 |
394 | def playing: Boolean = {
395 | val ret = if (AL10.alIsSource(alSource)) AL10.alGetSourcei(alSource, AL10.AL_SOURCE_STATE) == AL10.AL_PLAYING else false
396 | Util.checkALError()
397 | ret
398 | }
399 | def playing_=(playing: Boolean): Unit = if (playing) {
400 | if (AL10.alIsSource(alSource)) AL10.alSourcePlay(alSource)
401 | Util.checkALError()
402 | } else {
403 | if (AL10.alIsSource(alSource)) AL10.alSourcePause(alSource)
404 | Util.checkALError()
405 | }
406 |
407 | override def close(): Unit = {
408 | super.close()
409 |
410 | threadRunning = false
411 | wakeUpThread()
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/audio/VorbisDecoder.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import java.io.InputStream
4 | import com.jcraft.jogg.Packet
5 | import com.jcraft.jogg.Page
6 | import com.jcraft.jogg.StreamState
7 | import com.jcraft.jogg.SyncState
8 | import com.jcraft.jorbis.DspState
9 | import com.jcraft.jorbis.Block
10 | import com.jcraft.jorbis.Info
11 | import com.jcraft.jorbis.Comment
12 | import java.io.EOFException
13 | import java.io.FilterInputStream
14 | import java.io.IOException
15 | import java.nio.ByteBuffer
16 | import java.nio.ByteOrder
17 | import java.io.Closeable
18 |
19 | class VorbisDecoder private[games] (var in: InputStream, conv: Converter) extends Closeable {
20 | private val packet = new Packet
21 | private val page = new Page
22 | private val streamState = new StreamState
23 | private val syncState = new SyncState
24 |
25 | private val dspState = new DspState
26 | private val block = new Block(dspState)
27 | private val comment = new Comment
28 | private val info = new Info
29 |
30 | private var firstPage = true
31 | private var lastPage = false
32 |
33 | private val readBufferSize = 4096
34 |
35 | private def getNextPage(): Page = {
36 | syncState.pageout(page) match {
37 | case 0 => // need more data
38 | val index = syncState.buffer(readBufferSize)
39 | val buffer = syncState.data
40 | var read = in.read(buffer, index, readBufferSize)
41 | if (read < 0) {
42 | if (!lastPage) { System.err.println("Warning: End of stream reached before EOS page") }
43 | throw new EOFException()
44 | }
45 | val code = syncState.wrote(read)
46 | if (code < 0) throw new RuntimeException("Could not load the buffer. Code " + code)
47 | else getNextPage() // once the buffer is loaded successfully, try again
48 |
49 | case 1 => // page ok
50 | if (firstPage) {
51 | firstPage = false
52 | streamState.init(page.serialno())
53 | val code = streamState.reset()
54 | if (code < 0) throw new RuntimeException("Could not reset streamState. Code " + code)
55 |
56 | info.init()
57 | comment.init()
58 | }
59 | if (lastPage) System.err.println("Warning: EOS page already reached")
60 | else lastPage = page.eos() != 0
61 | page
62 |
63 | case x => throw new RuntimeException("Could not retrieve page from buffer. Code " + x)
64 | }
65 | }
66 |
67 | def getNextPacket(): Packet = streamState.packetout(packet) match {
68 | case 0 => // need a new page
69 | val code = streamState.pagein(getNextPage())
70 | if (code < 0) throw new RuntimeException("Could not load the page. Code " + code)
71 | else getNextPacket() // once a new page is loaded successfully, try again
72 |
73 | case 1 => packet // packet ok
74 | case x => throw new RuntimeException("Could not retrieve packet from page. Code " + x)
75 | }
76 |
77 | init()
78 |
79 | private def init() {
80 | try {
81 | syncState.init()
82 |
83 | for (i <- 1 to 3) { // Decode the three header packets
84 | val code = info.synthesis_headerin(comment, getNextPacket())
85 | if (code < 0) throw new RuntimeException("Could not synthesize the info. Code " + code)
86 | }
87 |
88 | if (dspState.synthesis_init(info) < 0) throw new RuntimeException("Could not init DspState")
89 | block.init(dspState)
90 |
91 | pcmIn = new Array[Array[Array[Float]]](1)
92 | indexIn = new Array[Int](info.channels)
93 | } catch {
94 | case e: Exception => throw new RuntimeException("Could not init the decoder", e)
95 | }
96 | }
97 |
98 | def rate: Int = info.rate
99 | def channels: Int = info.channels
100 |
101 | private var pcmIn: Array[Array[Array[Float]]] = _
102 | private var indexIn: Array[Int] = _
103 | private var remainingSamples = 0
104 | private var samplesRead = 0
105 |
106 | private def decodeNextPacket(): Unit = {
107 | if (dspState.synthesis_read(samplesRead) < 0) throw new RuntimeException("Could not acknowledge read samples")
108 | samplesRead = 0
109 |
110 | if (block.synthesis(this.getNextPacket()) < 0) throw new RuntimeException("Could not synthesize the block from packet")
111 | if (dspState.synthesis_blockin(block) < 0) throw new RuntimeException("Could not synthesize dspState from block")
112 |
113 | val availableSamples = dspState.synthesis_pcmout(pcmIn, indexIn)
114 | if (availableSamples < 0) throw new RuntimeException("Could not decode the block")
115 | //else if (availableSamples == 0) System.err.println("Warning: 0 samples decoded")
116 |
117 | remainingSamples = availableSamples
118 | }
119 |
120 | def read(out: ByteBuffer): Int = {
121 | while (remainingSamples <= 0) {
122 | decodeNextPacket()
123 | }
124 |
125 | def loop(count: Int): Int = {
126 | if (remainingSamples <= 0 || !(out.remaining() >= info.channels * conv.bytePerValue)) {
127 | count
128 | } else {
129 | for (channelNo <- 0 until info.channels) {
130 | val value = pcmIn(0)(channelNo)(indexIn(channelNo) + samplesRead)
131 | conv(value, out)
132 | }
133 |
134 | samplesRead += 1
135 | remainingSamples -= 1
136 |
137 | loop(count + 1)
138 | }
139 | }
140 |
141 | loop(0) * conv.bytePerValue * info.channels
142 | }
143 |
144 | def readFully(out: ByteBuffer): Int = {
145 | if (out.remaining() % (info.channels * conv.bytePerValue) != 0) throw new RuntimeException("Buffer capacity incorrect (remaining " + out.remaining() + ", required multiple of " + (info.channels * conv.bytePerValue) + ")")
146 |
147 | var total = 0
148 |
149 | while (out.remaining() > 0) {
150 | total += read(out)
151 | }
152 |
153 | total
154 | }
155 |
156 | def close(): Unit = {
157 | streamState.clear()
158 | block.clear()
159 | dspState.clear()
160 | info.clear()
161 | syncState.clear()
162 |
163 | in.close()
164 | }
165 | }
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/demo/Specifics.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | object Specifics {
4 | type WebSocketClient = transport.tyrus.WebSocketClient
5 | val platformName = "JVM"
6 | }
7 |
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/demoJVM/Launcher.scala:
--------------------------------------------------------------------------------
1 | package games.demoJVM
2 |
3 | import java.io.FileInputStream
4 | import java.io.File
5 | import java.io.EOFException
6 |
7 | import org.lwjgl.opengl._
8 |
9 | import games._
10 | import games.math
11 | import games.math.Vector3f
12 | import games.opengl._
13 | import games.audio._
14 | import games.input._
15 |
16 | import games.demo._
17 |
18 | import scala.concurrent.ExecutionContext.Implicits.global
19 |
20 | object Launcher {
21 | def main(args: Array[String]): Unit = {
22 | val itf = new EngineInterface {
23 | def initGL(): GLES2 = new GLES2LWJGL()
24 | def initAudio(): Context = new ALContext()
25 | def initKeyboard(): Keyboard = new KeyboardLWJGL()
26 | def initMouse(): Mouse = new MouseLWJGL()
27 | def initTouch(): Option[Touchscreen] = None
28 | def initAccelerometer: Option[Accelerometer] = None
29 | def continue(): Boolean = !Display.isCloseRequested()
30 | }
31 |
32 | val engine = new Engine(itf)
33 |
34 | Utils.startFrameListener(engine)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/input/Keyboard.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import org.lwjgl.input.{ Keyboard => LWJGLKeyboard }
4 |
5 | object KeyboardLWJGL {
6 | val mapper = new Keyboard.KeyMapper[Int](
7 | (Key.Space, LWJGLKeyboard.KEY_SPACE),
8 | (Key.Apostrophe, LWJGLKeyboard.KEY_APOSTROPHE),
9 | //(Key.Circumflex, LWJGLKeyboard.KEY_CIRCUMFLEX), // seems buggy
10 | (Key.Comma, LWJGLKeyboard.KEY_COMMA),
11 | (Key.Period, LWJGLKeyboard.KEY_PERIOD),
12 | (Key.Minus, LWJGLKeyboard.KEY_MINUS),
13 | (Key.Slash, LWJGLKeyboard.KEY_SLASH),
14 | (Key.N0, LWJGLKeyboard.KEY_0),
15 | (Key.N1, LWJGLKeyboard.KEY_1),
16 | (Key.N2, LWJGLKeyboard.KEY_2),
17 | (Key.N3, LWJGLKeyboard.KEY_3),
18 | (Key.N4, LWJGLKeyboard.KEY_4),
19 | (Key.N5, LWJGLKeyboard.KEY_5),
20 | (Key.N6, LWJGLKeyboard.KEY_6),
21 | (Key.N7, LWJGLKeyboard.KEY_7),
22 | (Key.N8, LWJGLKeyboard.KEY_8),
23 | (Key.N9, LWJGLKeyboard.KEY_9),
24 | (Key.SemiColon, LWJGLKeyboard.KEY_SEMICOLON),
25 | (Key.Equal, LWJGLKeyboard.KEY_EQUALS),
26 | (Key.A, LWJGLKeyboard.KEY_A),
27 | (Key.B, LWJGLKeyboard.KEY_B),
28 | (Key.C, LWJGLKeyboard.KEY_C),
29 | (Key.D, LWJGLKeyboard.KEY_D),
30 | (Key.E, LWJGLKeyboard.KEY_E),
31 | (Key.F, LWJGLKeyboard.KEY_F),
32 | (Key.G, LWJGLKeyboard.KEY_G),
33 | (Key.H, LWJGLKeyboard.KEY_H),
34 | (Key.I, LWJGLKeyboard.KEY_I),
35 | (Key.J, LWJGLKeyboard.KEY_J),
36 | (Key.K, LWJGLKeyboard.KEY_K),
37 | (Key.L, LWJGLKeyboard.KEY_L),
38 | (Key.M, LWJGLKeyboard.KEY_M),
39 | (Key.N, LWJGLKeyboard.KEY_N),
40 | (Key.O, LWJGLKeyboard.KEY_O),
41 | (Key.P, LWJGLKeyboard.KEY_P),
42 | (Key.Q, LWJGLKeyboard.KEY_Q),
43 | (Key.R, LWJGLKeyboard.KEY_R),
44 | (Key.S, LWJGLKeyboard.KEY_S),
45 | (Key.T, LWJGLKeyboard.KEY_T),
46 | (Key.U, LWJGLKeyboard.KEY_U),
47 | (Key.V, LWJGLKeyboard.KEY_V),
48 | (Key.W, LWJGLKeyboard.KEY_W),
49 | (Key.X, LWJGLKeyboard.KEY_X),
50 | (Key.Y, LWJGLKeyboard.KEY_Y),
51 | (Key.Z, LWJGLKeyboard.KEY_Z),
52 | (Key.BracketLeft, LWJGLKeyboard.KEY_LBRACKET),
53 | (Key.BracketRight, LWJGLKeyboard.KEY_RBRACKET),
54 | (Key.BackSlash, LWJGLKeyboard.KEY_BACKSLASH),
55 | (Key.GraveAccent, LWJGLKeyboard.KEY_GRAVE),
56 | (Key.Escape, LWJGLKeyboard.KEY_ESCAPE),
57 | (Key.Enter, LWJGLKeyboard.KEY_RETURN),
58 | (Key.Tab, LWJGLKeyboard.KEY_TAB),
59 | (Key.BackSpace, LWJGLKeyboard.KEY_BACK),
60 | (Key.Insert, LWJGLKeyboard.KEY_INSERT),
61 | (Key.Delete, LWJGLKeyboard.KEY_DELETE),
62 | (Key.Right, LWJGLKeyboard.KEY_RIGHT),
63 | (Key.Left, LWJGLKeyboard.KEY_LEFT),
64 | (Key.Down, LWJGLKeyboard.KEY_DOWN),
65 | (Key.Up, LWJGLKeyboard.KEY_UP),
66 | (Key.PageUp, LWJGLKeyboard.KEY_PRIOR),
67 | (Key.PageDown, LWJGLKeyboard.KEY_NEXT),
68 | (Key.Home, LWJGLKeyboard.KEY_HOME),
69 | (Key.End, LWJGLKeyboard.KEY_END),
70 | (Key.CapsLock, LWJGLKeyboard.KEY_CAPITAL),
71 | (Key.ScrollLock, LWJGLKeyboard.KEY_SCROLL),
72 | (Key.NumLock, LWJGLKeyboard.KEY_NUMLOCK),
73 | (Key.PrintScreen, LWJGLKeyboard.KEY_SYSRQ),
74 | (Key.Pause, LWJGLKeyboard.KEY_PAUSE),
75 | (Key.F1, LWJGLKeyboard.KEY_F1),
76 | (Key.F2, LWJGLKeyboard.KEY_F2),
77 | (Key.F3, LWJGLKeyboard.KEY_F3),
78 | (Key.F4, LWJGLKeyboard.KEY_F4),
79 | (Key.F5, LWJGLKeyboard.KEY_F5),
80 | (Key.F6, LWJGLKeyboard.KEY_F6),
81 | (Key.F7, LWJGLKeyboard.KEY_F7),
82 | (Key.F8, LWJGLKeyboard.KEY_F8),
83 | (Key.F9, LWJGLKeyboard.KEY_F9),
84 | (Key.F10, LWJGLKeyboard.KEY_F10),
85 | (Key.F11, LWJGLKeyboard.KEY_F11),
86 | (Key.F12, LWJGLKeyboard.KEY_F12),
87 | (Key.F13, LWJGLKeyboard.KEY_F13),
88 | (Key.F14, LWJGLKeyboard.KEY_F14),
89 | (Key.F15, LWJGLKeyboard.KEY_F15),
90 | (Key.F16, LWJGLKeyboard.KEY_F16),
91 | (Key.F17, LWJGLKeyboard.KEY_F17),
92 | (Key.F18, LWJGLKeyboard.KEY_F18),
93 | (Key.F19, LWJGLKeyboard.KEY_F19),
94 | // Nothing for F20 to F25
95 | (Key.Num0, LWJGLKeyboard.KEY_NUMPAD0),
96 | (Key.Num1, LWJGLKeyboard.KEY_NUMPAD1),
97 | (Key.Num2, LWJGLKeyboard.KEY_NUMPAD2),
98 | (Key.Num3, LWJGLKeyboard.KEY_NUMPAD3),
99 | (Key.Num4, LWJGLKeyboard.KEY_NUMPAD4),
100 | (Key.Num5, LWJGLKeyboard.KEY_NUMPAD5),
101 | (Key.Num6, LWJGLKeyboard.KEY_NUMPAD6),
102 | (Key.Num7, LWJGLKeyboard.KEY_NUMPAD7),
103 | (Key.Num8, LWJGLKeyboard.KEY_NUMPAD8),
104 | (Key.Num9, LWJGLKeyboard.KEY_NUMPAD9),
105 | (Key.NumDecimal, LWJGLKeyboard.KEY_DECIMAL),
106 | (Key.NumDivide, LWJGLKeyboard.KEY_DIVIDE),
107 | (Key.NumMultiply, LWJGLKeyboard.KEY_MULTIPLY),
108 | (Key.NumSubstract, LWJGLKeyboard.KEY_SUBTRACT),
109 | (Key.NumAdd, LWJGLKeyboard.KEY_ADD),
110 | (Key.NumEnter, LWJGLKeyboard.KEY_NUMPADENTER),
111 | (Key.NumEqual, LWJGLKeyboard.KEY_NUMPADEQUALS),
112 | (Key.ShiftLeft, LWJGLKeyboard.KEY_LSHIFT),
113 | (Key.ShiftRight, LWJGLKeyboard.KEY_RSHIFT),
114 | (Key.ControlLeft, LWJGLKeyboard.KEY_LCONTROL),
115 | (Key.ControlRight, LWJGLKeyboard.KEY_RCONTROL),
116 | (Key.AltLeft, LWJGLKeyboard.KEY_LMENU),
117 | (Key.AltRight, LWJGLKeyboard.KEY_RMENU),
118 | (Key.SuperLeft, LWJGLKeyboard.KEY_LMETA),
119 | (Key.SuperRight, LWJGLKeyboard.KEY_RMETA)
120 | //(Key.MenuLeft, 184 ???),
121 | //(Key.MenuRight, ???),
122 | )
123 | }
124 |
125 | class KeyboardLWJGL() extends Keyboard {
126 | LWJGLKeyboard.create()
127 |
128 | override def close(): Unit = {
129 | super.close()
130 | LWJGLKeyboard.destroy()
131 | }
132 |
133 | def isKeyDown(key: games.input.Key): Boolean = {
134 | LWJGLKeyboard.poll()
135 | KeyboardLWJGL.mapper.getForLocal(key) match {
136 | case Some(keyCode) => LWJGLKeyboard.isKeyDown(keyCode)
137 | case None => false // unsupported key
138 | }
139 | }
140 |
141 | def nextEvent(): Option[games.input.KeyboardEvent] = {
142 | if (LWJGLKeyboard.next()) {
143 | val keyCode = LWJGLKeyboard.getEventKey
144 | KeyboardLWJGL.mapper.getForRemote(keyCode) match {
145 | case Some(key) =>
146 | val down = LWJGLKeyboard.getEventKeyState()
147 | Some(KeyboardEvent(key, down))
148 |
149 | case None => nextEvent() // unsupported key, skip to the next event
150 | }
151 | } else None
152 | }
153 | }
--------------------------------------------------------------------------------
/demo/jvm/src/main/scala/games/input/Mouse.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import org.lwjgl.input.{ Mouse => LWJGLMouse }
4 |
5 | object MouseLWJGL {
6 | val mapper = new Mouse.ButtonMapper[Int](
7 | (Button.Left, 0),
8 | (Button.Right, 1),
9 | (Button.Middle, 2))
10 |
11 | private def getForLocal(button: Button): Int = button match {
12 | case Button.Aux(num) => num
13 | case _ => MouseLWJGL.mapper.getForLocal(button) match {
14 | case Some(num) => num
15 | case None => throw new RuntimeException("No known LWJGL code for button " + button)
16 | }
17 | }
18 |
19 | private def getForRemote(eventButton: Int): Button = MouseLWJGL.mapper.getForRemote(eventButton) match {
20 | case Some(button) => button
21 | case None => Button.Aux(eventButton)
22 | }
23 | }
24 |
25 | class MouseLWJGL() extends Mouse {
26 | LWJGLMouse.create()
27 |
28 | override def close(): Unit = {
29 | super.close()
30 | LWJGLMouse.destroy()
31 | }
32 |
33 | def position: games.input.Position = {
34 | val x = LWJGLMouse.getX()
35 | val y = LWJGLMouse.getY()
36 | Position(x, org.lwjgl.opengl.Display.getDisplayMode().getHeight() - y)
37 | }
38 | def deltaMotion: games.input.Position = {
39 | val dx = LWJGLMouse.getDX()
40 | val dy = LWJGLMouse.getDY()
41 | Position(dx, -dy)
42 | }
43 |
44 | def locked: Boolean = LWJGLMouse.isGrabbed()
45 | def locked_=(locked: Boolean): Unit = LWJGLMouse.setGrabbed(locked)
46 |
47 | def isButtonDown(button: games.input.Button): Boolean = LWJGLMouse.isButtonDown(MouseLWJGL.getForLocal(button))
48 | def nextEvent(): Option[games.input.MouseEvent] = {
49 | if (LWJGLMouse.next()) {
50 | val eventButton = LWJGLMouse.getEventButton()
51 | val eventWheel = LWJGLMouse.getEventDWheel()
52 |
53 | if (eventButton >= 0) {
54 | val button = MouseLWJGL.getForRemote(eventButton)
55 | val down = LWJGLMouse.getEventButtonState
56 | Some(ButtonEvent(button, down))
57 | } else if (eventWheel != 0) {
58 | if (eventWheel > 0) Some(WheelEvent(Wheel.Up))
59 | else Some(WheelEvent(Wheel.Down))
60 | } else nextEvent() // unknown event, skip to the next
61 | } else None
62 | }
63 |
64 | def isInside(): Boolean = LWJGLMouse.isInsideWindow()
65 | }
66 |
--------------------------------------------------------------------------------
/demo/server/src/main/scala/games/demo/server/Boot.scala:
--------------------------------------------------------------------------------
1 | package games.demo.server
2 |
3 | import akka.actor.{ ActorSystem, Props }
4 | import akka.io.IO
5 | import spray.can.Http
6 | import akka.pattern.ask
7 | import akka.util.Timeout
8 | import scala.concurrent.duration._
9 | import spray.can.server.UHttp
10 | import scala.concurrent.ExecutionContext.Implicits.global
11 |
12 | object Boot extends App {
13 | implicit val system = ActorSystem("SprayServer")
14 |
15 | val service = system.actorOf(Props[Service], "ListenerService")
16 |
17 | implicit val timeout = Timeout(5.seconds)
18 | IO(UHttp) ? Http.Bind(service, interface = "::0", port = 8080)
19 | }
20 |
--------------------------------------------------------------------------------
/demo/server/src/main/scala/games/demo/server/Service.scala:
--------------------------------------------------------------------------------
1 | package games.demo.server
2 |
3 | import games.demo.network
4 | import scala.concurrent.ExecutionContext.Implicits.global
5 | import scala.concurrent.{ Future, Promise, ExecutionContext }
6 | import akka.pattern.ask
7 | import akka.actor.ActorRef
8 | import akka.actor.Actor
9 | import scala.concurrent.duration._
10 | import akka.util.Timeout
11 | import spray.routing._
12 | import spray.http._
13 | import spray.http.CacheDirectives._
14 | import spray.http.HttpHeaders._
15 | import MediaTypes._
16 | import spray.can.websocket
17 | import spray.can.websocket.frame.{ Frame, TextFrame, BinaryFrame }
18 | import spray.can.websocket.FrameCommandFailed
19 | import spray.can.websocket.UpgradedToWebSocket
20 | import spray.can.websocket.FrameCommand
21 | import akka.actor.ActorRefFactory
22 | import spray.can.Http
23 | import akka.actor.Props
24 | import scala.collection.mutable
25 | import scala.collection.immutable
26 | import scala.concurrent.duration._
27 | import java.util.concurrent.Semaphore
28 | import scala.concurrent.Await
29 |
30 | sealed trait LocalMessage
31 |
32 | // Room messages
33 | sealed trait ToRoomMessage
34 | case class RegisterPlayer(playerActor: ConnectionActor) extends ToRoomMessage // request to register the player (expect responses)
35 | case class RemovePlayer(player: Player) extends ToRoomMessage // request to remove the player
36 | case object PingReminder extends ToRoomMessage // Room should ping its players
37 | case object UpdateReminder extends ToRoomMessage // Room should update the data of its players
38 |
39 | // Room responses to RegisterPlayer
40 | case object RoomFull extends LocalMessage // The room is full and can not accept more players
41 | case object RoomJoined extends LocalMessage // The room has accepted the player
42 |
43 | // Player messages
44 | sealed trait ToPlayerMessage
45 | case object SendPing extends ToPlayerMessage // Request a ping sent to the client
46 | case object Disconnected extends ToPlayerMessage // Signal that the client has disconnected
47 |
48 | // Player response to GetData
49 | case class PlayerDataResponse(projShots: immutable.Seq[network.ClientProjectileShot], projHits: immutable.Seq[network.ClientProjectileHit], data: network.PlayerData)
50 |
51 | object GlobalLogic {
52 | var players: Set[Player] = Set[Player]()
53 |
54 | private val lock = new Semaphore(1)
55 |
56 | private var nextRoomId = 0
57 | private val system = akka.actor.ActorSystem("GlobalLogic")
58 |
59 | private var currentRoom = newRoom()
60 |
61 | private def newRoom() = {
62 | val actor = system.actorOf(Props(classOf[Room], nextRoomId), name = "room" + nextRoomId)
63 | nextRoomId += 1
64 | actor
65 | }
66 |
67 | def registerPlayer(playerActor: ConnectionActor): Unit = {
68 | lock.acquire()
69 | implicit val timeout = Timeout(5.seconds)
70 |
71 | def tryRegister(): Unit = {
72 | val playerRegistered = currentRoom ? RegisterPlayer(playerActor)
73 | playerRegistered.onSuccess {
74 | case RoomJoined => // Ok, nothing to do
75 | lock.release()
76 |
77 | case RoomFull => // Room rejected the player, create a new room and try again
78 | currentRoom = newRoom()
79 | tryRegister()
80 | }
81 | playerRegistered.onFailure {
82 | case _ =>
83 | lock.release()
84 | }
85 | }
86 |
87 | tryRegister()
88 | }
89 | }
90 |
91 | class Room(val id: Int) extends Actor {
92 | println("Creating room " + id)
93 |
94 | val maxPlayers = 8
95 |
96 | val players: mutable.Set[Player] = mutable.Set()
97 |
98 | private def nextPlayerId(): Int = {
99 | def tryFrom(v: Int): Int = {
100 | if (players.forall { p => p.id != v }) v
101 | else tryFrom(v + 1)
102 | }
103 |
104 | tryFrom(1)
105 | }
106 |
107 | private var reportedFull = false
108 |
109 | private val pingIntervalMs = 5000 // Once every 5 seconds
110 | private val pingScheduler = this.context.system.scheduler.schedule(pingIntervalMs.milliseconds, pingIntervalMs.milliseconds, this.self, PingReminder)
111 |
112 | private val updateIntervalMs = 50 // about 20Hz refresh rate
113 | private val updateScheduler = this.context.system.scheduler.schedule(updateIntervalMs.milliseconds, updateIntervalMs.milliseconds, this.self, UpdateReminder)
114 |
115 | def receive: Receive = {
116 | case RegisterPlayer(playerActor) =>
117 | if (players.size >= maxPlayers || reportedFull) {
118 | reportedFull = true
119 | sender ! RoomFull
120 | } else {
121 | val newPlayerId = nextPlayerId()
122 | val player = new Player(playerActor, newPlayerId, this)
123 |
124 | players += player
125 |
126 | sender ! RoomJoined
127 |
128 | println("Player " + newPlayerId + " connected to room " + id)
129 | }
130 |
131 | case RemovePlayer(player) =>
132 | players -= player
133 | println("Player " + player.id + " disconnected from room " + id)
134 | if (players.isEmpty && reportedFull) {
135 | // This room is empty and will not receive further players, let's kill it
136 | pingScheduler.cancel()
137 | updateScheduler.cancel()
138 | context.stop(self)
139 | println("Closing room " + id)
140 | }
141 |
142 | case PingReminder =>
143 | players.foreach { player => player.actor.self ! SendPing }
144 |
145 | case UpdateReminder =>
146 | val playersResponse = players.map { player => player.getData() }
147 |
148 | val playersMsgData = playersResponse.map { response => response.data }.toSeq
149 |
150 | val projectileShotsData = (for (playerResponse <- playersResponse; projShot <- playerResponse.projShots) yield {
151 | val projId = network.ProjectileIdentifier(playerResponse.data.id, projShot.id)
152 | network.ProjectileShot(projId, projShot.position, projShot.orientation)
153 | }).toSeq
154 |
155 | val projectileHitsData = (for (playerResponse <- playersResponse; projHit <- playerResponse.projHits) yield {
156 | val projId = network.ProjectileIdentifier(playerResponse.data.id, projHit.id)
157 | network.ProjectileHit(projId, projHit.playerHitId)
158 | }).toSeq
159 |
160 | val events = immutable.Seq() ++ projectileShotsData ++ projectileHitsData
161 | val updateMsg = network.ServerUpdate(playersMsgData, events)
162 |
163 | players.foreach { player =>
164 | player.sendToClient(updateMsg)
165 | }
166 | }
167 | }
168 |
169 | class Player(val actor: ConnectionActor, val id: Int, val room: Room) {
170 | // Init
171 | actor.playerLogic = Some(this)
172 | sendToClient(network.ServerHello(id))
173 |
174 | private var lastPingTime: Option[Long] = None
175 |
176 | private var latency: Int = 0
177 | private var state: network.State = network.Absent
178 | private val projectileShotsData: mutable.Queue[network.ClientProjectileShot] = mutable.Queue()
179 | private val projectileHitsData: mutable.Queue[network.ClientProjectileHit] = mutable.Queue()
180 |
181 | def sendToClient(msg: network.ServerMessage): Unit = {
182 | val data = upickle.write(msg)
183 | actor.sendString(data)
184 | }
185 |
186 | def getData(): PlayerDataResponse = this.synchronized {
187 | val ret = PlayerDataResponse(immutable.Seq() ++ projectileShotsData, immutable.Seq() ++ projectileHitsData, network.PlayerData(this.id, this.latency, this.state))
188 | projectileShotsData.clear()
189 | projectileHitsData.clear()
190 | ret
191 | }
192 |
193 | def handleLocalMessage(msg: ToPlayerMessage): Unit = msg match {
194 | case Disconnected =>
195 | room.self ! RemovePlayer(this)
196 | //context.stop(self) // Done by the server at connection's termination?
197 |
198 | case SendPing =>
199 | lastPingTime = Some(System.currentTimeMillis())
200 | sendToClient(network.ServerPing)
201 | }
202 |
203 | def handleClientMessage(msg: network.ClientMessage): Unit = msg match {
204 | case network.ClientPong => // client's response
205 | for (time <- lastPingTime) {
206 | val elapsed = (System.currentTimeMillis() - time) / 2
207 | this.synchronized { latency = elapsed.toInt }
208 | lastPingTime = None
209 | }
210 | case x: network.ClientUpdate => this.synchronized { this.state = x.state }
211 | case x: network.ClientProjectileShot => this.synchronized { this.projectileShotsData += x }
212 | case x: network.ClientProjectileHit => this.synchronized { this.projectileHitsData += x }
213 | }
214 | }
215 |
216 | class ConnectionActor(val serverConnection: ActorRef) extends HttpServiceActor with websocket.WebSocketServerWorker {
217 | override def receive = handshaking orElse businessLogicNoUpgrade orElse closeLogic
218 |
219 | var playerLogic: Option[Player] = None
220 |
221 | def sendString(msg: String): Unit = send(TextFrame(msg))
222 |
223 | def businessLogic: Receive = {
224 | case localMsg: ToPlayerMessage => playerLogic match {
225 | case Some(logic) => logic.handleLocalMessage(localMsg)
226 | case None => println("Warning: connection not yet upgraded to player; can not process local message")
227 | }
228 |
229 | case tf: TextFrame => playerLogic match {
230 | case Some(logic) =>
231 | val payload = tf.payload
232 | val text = payload.utf8String
233 | if (!text.isEmpty()) {
234 | val clientMsg = upickle.read[network.ClientMessage](text)
235 | logic.handleClientMessage(clientMsg)
236 | }
237 | case None => println("Warning: connection not yet upgraded to player; can not process client message")
238 | }
239 |
240 | case x: FrameCommandFailed =>
241 | log.error("frame command failed", x)
242 |
243 | case UpgradedToWebSocket => playerLogic match {
244 | case None => GlobalLogic.registerPlayer(this)
245 | case _ => println("Warning: the connection has already been upgraded to player")
246 | }
247 |
248 | case x: Http.ConnectionClosed => for (logic <- playerLogic) {
249 | logic.handleLocalMessage(Disconnected)
250 | playerLogic = None
251 | }
252 | }
253 |
254 | def businessLogicNoUpgrade: Receive = {
255 | implicit val refFactory: ActorRefFactory = context
256 | runRoute(cachedRoute)
257 | }
258 |
259 | def cachedRoute = respondWithHeader(`Cache-Control`(`public`, `no-cache`)) { myRoute }
260 |
261 | val myRoute =
262 | path("") {
263 | respondWithMediaType(`text/html`) {
264 | getFromFile("../../demoJS-launcher/index.html")
265 | }
266 | } ~
267 | path("fast") {
268 | respondWithMediaType(`text/html`) {
269 | getFromFile("../../demoJS-launcher/index-fastopt.html")
270 | }
271 | } ~
272 | path("code" / Rest) { file =>
273 | val path = "../js/target/scala-2.11/" + file
274 | getFromFile(path)
275 | } ~
276 | path("resources" / Rest) { file =>
277 | val path = "../shared/src/main/resources/" + file
278 | getFromFile(path)
279 | }
280 | }
281 |
282 | class Service extends Actor {
283 |
284 | def receive = {
285 | case Http.Connected(remoteAddress, localAddress) =>
286 | val curSender = sender()
287 | val conn = context.actorOf(Props(classOf[ConnectionActor], curSender))
288 | curSender ! Http.Register(conn)
289 | }
290 | }
291 |
292 |
--------------------------------------------------------------------------------
/demo/shared-server/src/main/scala/games/demo/network/Message.scala:
--------------------------------------------------------------------------------
1 | package games.demo.network
2 |
3 | case class Vector2(x: Float, y: Float)
4 |
5 | case class ProjectileIdentifier(playerId: Int, projectileId: Int)
6 |
7 | sealed trait State
8 | case object Absent extends State
9 | case class Present(position: Vector2, velocity: Vector2, orientation: Float, health: Float) extends State
10 |
11 | case class PlayerData(id: Int, latency: Int, state: State)
12 |
13 | sealed trait Event
14 | case class ProjectileShot(id: ProjectileIdentifier, position: Vector2, orientation: Float) extends Event
15 | case class ProjectileHit(id: ProjectileIdentifier, playerHit: Int) extends Event
16 |
17 | sealed trait NetworkMessage
18 | sealed trait ClientMessage extends NetworkMessage
19 | sealed trait ServerMessage extends NetworkMessage
20 | // Server -> Client
21 | case object ServerPing extends ServerMessage
22 | case class ServerHello(playerId: Int) extends ServerMessage
23 | case class ServerUpdate(players: Seq[PlayerData], newEvents: Seq[Event]) extends ServerMessage
24 | // Server <- Client
25 | case object ClientPong extends ClientMessage
26 | case class ClientUpdate(state: State) extends ClientMessage
27 | case class ClientProjectileShot(id: Int, position: Vector2, orientation: Float) extends ClientMessage
28 | case class ClientProjectileHit(id: Int, playerHitId: Int) extends ClientMessage
29 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/config:
--------------------------------------------------------------------------------
1 | server=ws://localhost:8080/
2 | models=/games/demo/models
3 | shaders=/games/demo/shaders
4 | map=/games/demo/maps/map1
5 | shotSound=/games/demo/sounds/flashkit/Sniper_R-MelonHea-7518_hifi.ogg
6 | damageSound=/games/demo/sounds/flashkit/Spark_1-kayden_r-8968_hifi.ogg
7 | invulnerabilityTimeMs=5000
8 | shotIntervalMs=250
9 | shotToKill=5
10 | maxForwardSpeed=4
11 | maxBackwardSpeed=2
12 | maxLateralSpeed=3
13 | maxTouchTimeToShotMs=100
14 | projectileVelocity=15
15 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/maps/map1:
--------------------------------------------------------------------------------
1 | #player_orientation:1=270
2 | #player_orientation:2=90
3 | #player_orientation:3=0
4 | #player_orientation:4=180
5 | #player_orientation:5=90
6 | #player_orientation:6=270
7 | #player_orientation:7=180
8 | #player_orientation:8=0
9 |
10 | 7 1x xxx5
11 | xx x xxxx x
12 | x xxx x 4
13 | xxxx xxxxxx
14 | x xxxxx x
15 | x xxxxx xx
16 | xx xxxxx x
17 | x xxxxx x
18 | xxxxxx xxxx
19 | 3 x xxx x
20 | x xxxx x xx
21 | 6xxx x2 8
22 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/bullet/bullet.mtl:
--------------------------------------------------------------------------------
1 | # Blender MTL File: 'bullet.blend'
2 | # Material Count: 1
3 |
4 | newmtl [player]
5 | Ns 96.078431
6 | Ka 0.500000 0.500000 0.500000
7 | Kd 0.200000 0.200000 0.200000
8 | Ks 0.500000 0.500000 0.500000
9 | Ni 1.000000
10 | d 1.000000
11 | illum 2
12 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/bullet/bullet.obj:
--------------------------------------------------------------------------------
1 | # Blender v2.69 (sub 0) OBJ File: 'bullet.blend'
2 | # www.blender.org
3 | mtllib bullet.mtl
4 | o Bullet
5 | v -0.000000 -0.000000 -0.600000
6 | v -0.000000 -0.141421 -0.400000
7 | v -0.141421 -0.000000 -0.400000
8 | v 0.141421 0.000000 -0.400000
9 | v -0.000000 0.141421 -0.400000
10 | v -0.000000 0.000000 0.000000
11 | vn -0.632455 -0.632456 -0.447214
12 | vn 0.632455 0.632456 -0.447214
13 | vn 0.632456 -0.632455 -0.447213
14 | vn 0.685994 0.685994 0.242536
15 | vn -0.632456 0.632455 -0.447214
16 | vn -0.685994 0.685994 0.242536
17 | vn 0.685994 -0.685994 0.242536
18 | vn -0.685994 -0.685994 0.242536
19 | usemtl [player]
20 | s off
21 | f 1//1 2//1 3//1
22 | f 1//2 5//2 4//2
23 | f 1//3 4//3 2//3
24 | f 4//4 5//4 6//4
25 | f 3//5 5//5 1//5
26 | f 5//6 3//6 6//6
27 | f 2//7 4//7 6//7
28 | f 3//8 2//8 6//8
29 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/bullet/main:
--------------------------------------------------------------------------------
1 | name=Bullet
2 | obj=bullet.obj
3 | mtl=bullet.mtl
4 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/character/character.mtl:
--------------------------------------------------------------------------------
1 | # Blender MTL File: 'character.out.blend'
2 | # Material Count: 3
3 |
4 | newmtl [player]
5 | Ns 96.078431
6 | Ka 0.500000 0.500000 0.500000
7 | Kd 0.200000 0.200000 0.200000
8 | Ks 0.500000 0.500000 0.500000
9 | Ni 1.000000
10 | d 1.000000
11 | illum 2
12 |
13 | newmtl base
14 | Ns 96.078431
15 | Ka 0.500000 0.500000 0.500000
16 | Kd 0.200000 0.200000 0.200000
17 | Ks 0.500000 0.500000 0.500000
18 | Ni 1.000000
19 | d 1.000000
20 | illum 2
21 |
22 | newmtl visor
23 | Ns 96.078431
24 | Ka 0.000000 0.000000 0.000000
25 | Kd 0.000000 0.000000 0.000000
26 | Ks 0.500000 0.500000 0.500000
27 | Ni 1.000000
28 | d 1.000000
29 | illum 2
30 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/character/main:
--------------------------------------------------------------------------------
1 | name=Character
2 | obj=character.obj
3 | mtl=character.mtl
4 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/floor/floor.mtl:
--------------------------------------------------------------------------------
1 | # Blender MTL File: 'floor.blend'
2 | # Material Count: 2
3 |
4 | newmtl wallBase
5 | Ns 96.078431
6 | Ka 0.500000 0.500000 0.500000
7 | Kd 0.140000 0.140000 0.140000
8 | Ks 0.500000 0.500000 0.500000
9 | Ni 1.000000
10 | d 1.000000
11 | illum 2
12 |
13 | newmtl wallDepth
14 | Ns 96.078431
15 | Ka 0.000000 0.000000 0.000000
16 | Kd 0.032568 0.020657 0.046189
17 | Ks 0.500000 0.500000 0.500000
18 | Ni 1.000000
19 | d 1.000000
20 | illum 2
21 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/floor/floor.obj:
--------------------------------------------------------------------------------
1 | # Blender v2.69 (sub 0) OBJ File: 'floor.blend'
2 | # www.blender.org
3 | mtllib floor.mtl
4 | o Floor
5 | v 1.000000 0.000000 1.000000
6 | v -1.000000 0.000000 1.000000
7 | v 1.000000 0.000000 -1.000000
8 | v -1.000000 0.000000 -1.000000
9 | v 1.000000 2.000000 1.000000
10 | v -1.000000 2.000000 1.000000
11 | v 1.000000 2.000000 -1.000000
12 | v -1.000000 2.000000 -1.000000
13 | v 0.833333 2.100000 0.833333
14 | v -0.833333 2.100000 0.833333
15 | v 0.833333 2.100000 -0.833333
16 | v -0.833333 2.100000 -0.833333
17 | v -0.833333 2.000000 0.833333
18 | v 0.833333 2.000000 0.833333
19 | v -0.833333 2.000000 -0.833333
20 | v 0.833333 2.000000 -0.833333
21 | vn 0.000000 1.000000 0.000000
22 | vn 0.000000 0.000000 -1.000000
23 | vn 1.000000 0.000000 0.000000
24 | vn 0.000000 0.000000 1.000000
25 | vn -1.000000 0.000000 0.000000
26 | vn 0.000000 -1.000000 0.000000
27 | usemtl wallBase
28 | s off
29 | f 2//1 1//1 3//1 4//1
30 | f 14//2 13//2 10//2 9//2
31 | f 13//3 15//3 12//3 10//3
32 | f 15//4 16//4 11//4 12//4
33 | f 16//5 14//5 9//5 11//5
34 | f 5//6 6//6 13//6 14//6
35 | f 15//6 13//6 6//6 8//6
36 | f 8//6 7//6 16//6 15//6
37 | f 7//6 5//6 14//6 16//6
38 | usemtl wallDepth
39 | f 9//6 10//6 12//6 11//6
40 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/floor/main:
--------------------------------------------------------------------------------
1 | name=Floor
2 | obj=floor.obj
3 | mtl=floor.mtl
4 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/list:
--------------------------------------------------------------------------------
1 | bullet
2 | character
3 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/wall/main:
--------------------------------------------------------------------------------
1 | name=Wall
2 | obj=wall.obj
3 | mtl=wall.mtl
4 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/wall/wall.mtl:
--------------------------------------------------------------------------------
1 | # Blender MTL File: 'wall.blend'
2 | # Material Count: 2
3 |
4 | newmtl wallBase
5 | Ns 96.078431
6 | Ka 0.500000 0.500000 0.500000
7 | Kd 0.140000 0.140000 0.140000
8 | Ks 0.500000 0.500000 0.500000
9 | Ni 1.000000
10 | d 1.000000
11 | illum 2
12 |
13 | newmtl wallDepth
14 | Ns 96.078431
15 | Ka 0.000000 0.000000 0.000000
16 | Kd 0.032568 0.020657 0.046189
17 | Ks 0.500000 0.500000 0.500000
18 | Ni 1.000000
19 | d 1.000000
20 | illum 2
21 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/models/wall/wall.obj:
--------------------------------------------------------------------------------
1 | # Blender v2.69 (sub 0) OBJ File: 'wall.blend'
2 | # www.blender.org
3 | mtllib wall.mtl
4 | o Wall
5 | v 1.000000 2.000000 0.000000
6 | v -1.000000 2.000000 0.000000
7 | v 1.000000 0.000000 -0.000000
8 | v -1.000000 0.000000 -0.000000
9 | v -0.833333 0.194858 -0.000000
10 | v 0.833333 0.194858 -0.000000
11 | v -0.833333 1.581525 0.000000
12 | v 0.833333 1.581525 0.000000
13 | v 0.527778 1.914858 0.000000
14 | v -0.527778 1.914858 0.000000
15 | v 0.720934 0.286131 0.146920
16 | v -0.720934 0.286131 0.146920
17 | v -0.720934 1.411339 0.146920
18 | v 0.720934 1.411339 0.146920
19 | v 0.456592 1.681821 0.146920
20 | v -0.456592 1.681821 0.146920
21 | vn 0.000000 0.849430 -0.527701
22 | vn -0.794231 0.000000 -0.607616
23 | vn 0.794231 0.000000 -0.607616
24 | vn -0.439471 -0.402848 -0.802857
25 | vn 0.439471 -0.402848 -0.802857
26 | vn 0.000000 -0.533314 -0.845917
27 | vn 0.000000 0.000000 -1.000000
28 | vn -0.405055 -0.395860 -0.824151
29 | vn 0.405055 -0.395860 -0.824151
30 | vn 0.000000 -0.533315 -0.845917
31 | usemtl wallBase
32 | s off
33 | f 6//1 5//1 11//1
34 | f 8//2 6//2 11//2
35 | f 5//3 7//3 12//3
36 | f 9//4 8//4 14//4
37 | f 7//5 10//5 13//5
38 | f 10//6 9//6 16//6
39 | f 1//7 8//7 9//7
40 | f 7//7 2//7 10//7
41 | f 3//7 6//7 1//7
42 | f 3//7 4//7 6//7
43 | f 4//7 2//7 5//7
44 | f 1//7 9//7 2//7
45 | f 5//1 12//1 11//1
46 | f 14//2 8//2 11//2
47 | f 7//3 13//3 12//3
48 | f 15//8 9//8 14//8
49 | f 10//9 16//9 13//9
50 | f 9//10 15//10 16//10
51 | f 6//7 8//7 1//7
52 | f 4//7 5//7 6//7
53 | f 2//7 7//7 5//7
54 | f 9//7 10//7 2//7
55 | usemtl wallDepth
56 | f 14//7 11//7 12//7
57 | f 14//7 12//7 13//7
58 | f 15//7 14//7 16//7
59 | f 14//7 13//7 16//7
60 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/shaders/list:
--------------------------------------------------------------------------------
1 | simple3d
2 | simple2d
3 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/shaders/simple2d/fragment.c:
--------------------------------------------------------------------------------
1 | #ifdef GL_ES
2 | precision mediump float;
3 | #endif
4 |
5 | uniform vec3 color;
6 |
7 | void main(void) {
8 | gl_FragColor = vec4(color, 1.0);
9 | }
10 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/shaders/simple2d/vertex.c:
--------------------------------------------------------------------------------
1 | uniform float scaleX;
2 | uniform float scaleY;
3 |
4 | uniform mat3 transform;
5 |
6 | attribute vec2 position;
7 |
8 | void main(void) {
9 | vec2 transformed = (transform * vec3(position, 1.0)).xy;
10 | gl_Position = vec4(transformed.x * scaleX, transformed.y * scaleY, 0.0, 1.0);
11 | }
12 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/shaders/simple3d/fragment.c:
--------------------------------------------------------------------------------
1 | #ifdef GL_ES
2 | precision mediump float;
3 | #endif
4 |
5 | uniform vec3 ambientColor;
6 | uniform vec3 diffuseColor;
7 |
8 | varying vec3 varNormal;
9 | varying vec3 varView;
10 |
11 | void main(void) {
12 | gl_FragColor = vec4(ambientColor + diffuseColor * dot(normalize(varView), normalize(varNormal)), 1.0);
13 | }
14 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/shaders/simple3d/vertex.c:
--------------------------------------------------------------------------------
1 | uniform mat4 projection;
2 | uniform mat4 modelView;
3 | uniform mat3 normalModelView;
4 |
5 | attribute vec3 position;
6 | attribute vec3 normal;
7 |
8 | varying vec3 varNormal;
9 | varying vec3 varView;
10 |
11 | void main(void) {
12 | vec4 pos = modelView * vec4(position, 1.0);
13 | gl_Position = projection * pos;
14 | varNormal = normalize(normalModelView * normal);
15 | varView = -pos.xyz;
16 | }
17 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/sounds/flashkit/NOTICE.md:
--------------------------------------------------------------------------------
1 | The content of this folder is the property of their respective owner
2 |
3 | * Sniper_R-MelonHea-7518_hifi.ogg is the property of [MelonHead](http://www.flashkit.com/soundfx/Mayhem/Rifles/Sniper_R-MelonHea-7518/index.php)
4 | * Spark_1-kayden_r-8968_hifi.ogg is the property of [Kayden Riggs](http://www.flashkit.com/soundfx/Electronic/Electricity/Spark_1-kayden_r-8968/index.php)
5 |
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/sounds/flashkit/Sniper_R-MelonHea-7518_hifi.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/shared/src/main/resources/games/demo/sounds/flashkit/Sniper_R-MelonHea-7518_hifi.ogg
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/sounds/flashkit/Spark_1-kayden_r-8968_hifi.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/shared/src/main/resources/games/demo/sounds/flashkit/Spark_1-kayden_r-8968_hifi.ogg
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/sounds/test_mono.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/shared/src/main/resources/games/demo/sounds/test_mono.ogg
--------------------------------------------------------------------------------
/demo/shared/src/main/resources/games/demo/sounds/test_stereo.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/demo/shared/src/main/resources/games/demo/sounds/test_stereo.ogg
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/Resource.scala:
--------------------------------------------------------------------------------
1 | package games
2 |
3 | case class Resource(name: String)
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/Utils.scala:
--------------------------------------------------------------------------------
1 | package games
2 |
3 | import scala.concurrent.{ Future, Promise, ExecutionContext }
4 | import java.nio.ByteBuffer
5 | import games.opengl.GLES2
6 | import games.opengl.Token
7 |
8 | case class FrameEvent(elapsedTime: Float)
9 |
10 | trait FrameListener {
11 | val loopExecutionContext: ExecutionContext = Utils.getLoopThreadExecutionContext()
12 |
13 | def onCreate(): Future[Unit]
14 | def onDraw(fe: FrameEvent): Boolean
15 | def onClose(): Unit
16 | }
17 |
18 | trait UtilsRequirements {
19 | private[games] def getLoopThreadExecutionContext(): ExecutionContext
20 | def getBinaryDataFromResource(res: games.Resource)(implicit ec: ExecutionContext): scala.concurrent.Future[java.nio.ByteBuffer]
21 | def getTextDataFromResource(res: games.Resource)(implicit ec: ExecutionContext): scala.concurrent.Future[String]
22 | def loadTexture2DFromResource(res: games.Resource, texture: games.opengl.Token.Texture, gl: games.opengl.GLES2, openglExecutionContext: ExecutionContext)(implicit ec: ExecutionContext): scala.concurrent.Future[Unit]
23 | def startFrameListener(fl: games.FrameListener): Unit
24 | }
25 |
26 | object Utils extends UtilsImpl
27 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/audio/Context.scala:
--------------------------------------------------------------------------------
1 | package games.audio
2 |
3 | import scala.concurrent.{ Promise, Future, ExecutionContext }
4 | import scala.collection.mutable
5 |
6 | import games.Resource
7 | import games.math.Vector3f
8 | import java.io.Closeable
9 |
10 | import java.nio.ByteBuffer
11 |
12 | abstract sealed class Format
13 |
14 | object Format {
15 | case object Float32 extends Format
16 | }
17 |
18 | abstract class Context extends Closeable {
19 | def prepareStreamingData(res: games.Resource): Future[games.audio.Data]
20 | def prepareBufferedData(res: games.Resource): Future[games.audio.BufferedData]
21 | def prepareRawData(data: java.nio.ByteBuffer, format: games.audio.Format, channels: Int, freq: Int): Future[games.audio.BufferedData]
22 |
23 | private def tryFutures[T](res: TraversableOnce[games.Resource], fun: Resource => Future[T])(implicit ec: ExecutionContext): Future[T] = {
24 | val promise = Promise[T]
25 |
26 | val iterator = res.toIterator
27 |
28 | def tryNext(): Unit = if (iterator.hasNext) {
29 | val nextResource = iterator.next()
30 | val dataFuture = fun(nextResource)
31 | dataFuture.onSuccess { case v => promise.success(v) }
32 | dataFuture.onFailure { case t => tryNext() }
33 | } else {
34 | promise.failure(new RuntimeException("No usable resource in " + res))
35 | }
36 |
37 | tryNext()
38 |
39 | promise.future
40 | }
41 |
42 | def prepareStreamingData(res: scala.collection.TraversableOnce[games.Resource])(implicit ec: scala.concurrent.ExecutionContext): Future[games.audio.Data] = tryFutures(res, prepareStreamingData(_))
43 | def prepareBufferedData(res: scala.collection.TraversableOnce[games.Resource])(implicit ec: scala.concurrent.ExecutionContext): Future[games.audio.BufferedData] = tryFutures(res, prepareBufferedData(_))
44 |
45 | def createSource(): games.audio.Source
46 | def createSource3D(): games.audio.Source3D
47 |
48 | def listener: games.audio.Listener
49 |
50 | def volume: Float
51 | def volume_=(volume: Float): Unit
52 |
53 | private[games] val datas: mutable.Set[Data] = mutable.Set()
54 | private[games] def registerData(data: Data): Unit = datas += data
55 | private[games] def unregisterData(data: Data): Unit = datas -= data
56 |
57 | private[games] val sources: mutable.Set[Source] = mutable.Set()
58 | private[games] def registerSource(source: Source): Unit = sources += source
59 | private[games] def unregisterSource(source: Source): Unit = sources -= source
60 |
61 | def close(): Unit = {
62 | for (data <- this.datas) {
63 | data.close()
64 | }
65 | for (source <- this.sources) {
66 | source.close()
67 | }
68 |
69 | datas.clear()
70 | sources.clear()
71 | }
72 | }
73 |
74 | sealed trait Spatial {
75 | def position: games.math.Vector3f
76 | def position_=(position: games.math.Vector3f)
77 | }
78 |
79 | abstract class Listener extends Closeable with Spatial {
80 | def up: games.math.Vector3f
81 |
82 | def orientation: games.math.Vector3f
83 |
84 | def setOrientation(orientation: games.math.Vector3f, up: games.math.Vector3f): Unit
85 |
86 | def close(): Unit = {}
87 | }
88 |
89 | abstract class Data extends Closeable {
90 | def attach(source: games.audio.Source): scala.concurrent.Future[games.audio.Player]
91 |
92 | private[games] val players: mutable.Set[Player] = mutable.Set()
93 | private[games] def registerPlayer(player: Player): Unit = players += player
94 | private[games] def unregisterPlayer(player: Player): Unit = players -= player
95 |
96 | def close(): Unit = {
97 | for (player <- players) {
98 | player.close()
99 | }
100 |
101 | players.clear()
102 | }
103 | }
104 |
105 | abstract class BufferedData extends Data {
106 | def attachNow(source: games.audio.Source): games.audio.Player
107 | def attach(source: games.audio.Source): scala.concurrent.Future[games.audio.Player] = try {
108 | Future.successful(this.attachNow(source))
109 | } catch {
110 | case t: Throwable => Future.failed(t)
111 | }
112 | }
113 |
114 | abstract class Player extends Closeable {
115 | def playing: Boolean
116 | def playing_=(playing: Boolean): Unit
117 |
118 | def volume: Float
119 | def volume_=(volume: Float): Unit
120 |
121 | def loop: Boolean
122 | def loop_=(loop: Boolean): Unit
123 |
124 | def pitch: Float
125 | def pitch_=(pitch: Float): Unit
126 |
127 | def close(): Unit = {
128 | this.playing = false
129 | }
130 | }
131 |
132 | abstract class Source extends Closeable {
133 | private[games] val players: mutable.Set[Player] = mutable.Set()
134 | private[games] def registerPlayer(player: Player): Unit = players += player
135 | private[games] def unregisterPlayer(player: Player): Unit = players -= player
136 |
137 | def close(): Unit = {
138 | for (player <- players) {
139 | player.close()
140 | }
141 |
142 | players.clear()
143 | }
144 | }
145 | abstract class Source3D extends Source with Spatial
146 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/demo/Data.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | import games.math.Vector3f
4 |
5 | import scala.collection.immutable
6 | import scala.collection.mutable
7 |
8 | object Data {
9 | val colors: immutable.Map[Int, Vector3f] = immutable.Map(
10 | 1 -> new Vector3f(1f, 0f, 0f), // Red for player 1
11 | 2 -> new Vector3f(0f, 0f, 1f), // Blue for player 2
12 | 3 -> new Vector3f(0f, 1f, 0f), // Green for player 3
13 | 4 -> new Vector3f(1f, 1f, 0f), // Yellow for player 4
14 | 5 -> new Vector3f(0f, 1f, 1f), // Cyan for player 5
15 | 6 -> new Vector3f(1f, 0.5f, 0f), // Orange for player 6
16 | 7 -> new Vector3f(0.8f, 0f, 0.8f), // Purple for player 7
17 | 8 -> new Vector3f(1f, 0f, 0.5f) // Pink for player 8
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/demo/Map.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | import games._
4 | import games.math.{ Vector2f, Vector3f }
5 | import scala.concurrent.{ Future, ExecutionContext }
6 |
7 | import scala.collection.immutable
8 | import scala.collection.mutable
9 |
10 | object Map {
11 | final val roomSize: Float = 2f
12 | final val roomHalfSize: Float = roomSize / 2
13 |
14 | def coordinates(pos: Vector2f): (Int, Int) = (Math.floor(pos.x / Map.roomSize).toInt, Math.floor(pos.y / Map.roomSize).toInt)
15 |
16 | def load(resourceMap: Resource)(implicit ec: ExecutionContext): Future[Map] = {
17 | val mapFileFuture: Future[String] = Utils.getTextDataFromResource(resourceMap)
18 |
19 | for (mapFile <- mapFileFuture) yield {
20 | val lines = mapFile.lines
21 |
22 | val rooms: mutable.ArrayBuffer[Room] = mutable.ArrayBuffer()
23 | val startPositions: mutable.Map[Int, Room] = mutable.Map()
24 | val startOrientations: mutable.Map[Int, Float] = mutable.Map()
25 |
26 | var mapDataLine = false
27 |
28 | var y: Int = 0
29 | for (line <- lines) {
30 | if (line.startsWith("#")) {
31 | val rest = line.substring(1)
32 | val lineTokens = rest.trim().split(":", 2)
33 | if (lineTokens.size == 2) {
34 |
35 | val key = lineTokens(0)
36 | val value = lineTokens(1)
37 |
38 | key match {
39 | case "player_orientation" =>
40 | val orientationTokens = value.split("=", 2)
41 | val playerId = orientationTokens(0).toInt
42 | val orientation = orientationTokens(1).toFloat
43 | startOrientations += (playerId -> orientation)
44 |
45 | case _ => Console.err.println("Unknown config key '" + key + "' in map line: " + line)
46 | }
47 | }
48 |
49 | } else if (mapDataLine || !line.trim().isEmpty()) {
50 | mapDataLine = true
51 |
52 | var x: Int = 0
53 | for (char <- line) {
54 |
55 | char match {
56 | case 'x' => rooms += new Room(x, y)
57 |
58 | case v if Character.isDigit(v) =>
59 | val number = v - '0'
60 | val room = new Room(x, y)
61 | rooms += room
62 | startPositions += (number -> room)
63 |
64 | case _ =>
65 | }
66 |
67 | x += 1
68 | }
69 | y += 1
70 | }
71 | }
72 |
73 | val width = rooms.map(_.x).reduce(Math.max) + 1
74 | val height = rooms.map(_.y).reduce(Math.max) + 1
75 |
76 | val array = Array.ofDim[Option[Room]](width, height)
77 |
78 | for (
79 | x <- 0 until width;
80 | y <- 0 until height
81 | ) {
82 | array(x)(y) = rooms.find { r => r.x == x && r.y == y }
83 | }
84 |
85 | new Map(array, startPositions.toMap, startOrientations.toMap, width, height)
86 | }
87 | }
88 | }
89 |
90 | class Room(val x: Int, val y: Int) {
91 | lazy val center = new Vector2f(Map.roomSize * x + Map.roomHalfSize, Map.roomSize * y + Map.roomHalfSize)
92 | }
93 |
94 | class ContinuousWall(val position: Vector2f, val length: Float) {
95 | val halfLength = length / 2
96 | }
97 |
98 | class Map(val rooms: Array[Array[Option[Room]]], val startPositions: immutable.Map[Int, Room], val startOrientations: immutable.Map[Int, Float], val width: Int, val height: Int) {
99 | def roomAt(x: Int, y: Int): Option[Room] = if (x >= 0 && x < width && y >= 0 && y < height) rooms(x)(y) else None
100 | def roomAt(pos: Vector2f): Option[Room] = {
101 | val (x, y) = Map.coordinates(pos)
102 | roomAt(x, y)
103 | }
104 |
105 | val definedRooms = for (
106 | x <- 0 until width;
107 | y <- 0 until height;
108 | room <- rooms(x)(y)
109 | ) yield room
110 |
111 | def hasLWall(room: Room): Boolean = roomAt(room.x - 1, room.y).isEmpty
112 | def hasRWall(room: Room): Boolean = roomAt(room.x + 1, room.y).isEmpty
113 | def hasTWall(room: Room): Boolean = roomAt(room.x, room.y - 1).isEmpty
114 | def hasBWall(room: Room): Boolean = roomAt(room.x, room.y + 1).isEmpty
115 |
116 | val (floors, lWalls, rWalls, tWalls, bWalls) = {
117 | val floors: mutable.ArrayBuffer[Vector2f] = mutable.ArrayBuffer()
118 | val lWalls: mutable.ArrayBuffer[Vector2f] = mutable.ArrayBuffer() // Left walls
119 | val rWalls: mutable.ArrayBuffer[Vector2f] = mutable.ArrayBuffer() // Right walls
120 | val tWalls: mutable.ArrayBuffer[Vector2f] = mutable.ArrayBuffer() // Top walls
121 | val bWalls: mutable.ArrayBuffer[Vector2f] = mutable.ArrayBuffer() // Bottom walls
122 |
123 | for (
124 | room <- definedRooms
125 | ) {
126 | floors += new Vector2f(Map.roomSize * room.x + Map.roomHalfSize, Map.roomSize * room.y + Map.roomHalfSize)
127 | if (hasLWall(room)) lWalls += new Vector2f(Map.roomSize * room.x, Map.roomSize * room.y + Map.roomHalfSize)
128 | if (hasRWall(room)) rWalls += new Vector2f(Map.roomSize * (room.x + 1), Map.roomSize * room.y + Map.roomHalfSize)
129 | if (hasTWall(room)) tWalls += new Vector2f(Map.roomSize * room.x + Map.roomHalfSize, Map.roomSize * room.y)
130 | if (hasBWall(room)) bWalls += new Vector2f(Map.roomSize * room.x + Map.roomHalfSize, Map.roomSize * (room.y + 1))
131 | }
132 |
133 | (floors.toArray, lWalls.toArray, rWalls.toArray, tWalls.toArray, bWalls.toArray)
134 | }
135 |
136 | val (clWalls, crWalls, ctWalls, cbWalls) = {
137 | val lWalls: mutable.ArrayBuffer[ContinuousWall] = mutable.ArrayBuffer() // Left walls
138 | val rWalls: mutable.ArrayBuffer[ContinuousWall] = mutable.ArrayBuffer() // Right walls
139 | val tWalls: mutable.ArrayBuffer[ContinuousWall] = mutable.ArrayBuffer() // Top walls
140 | val bWalls: mutable.ArrayBuffer[ContinuousWall] = mutable.ArrayBuffer() // Bottom walls
141 |
142 | // TODO FIXME too much copy-paste, make this more modular
143 |
144 | for (y <- 0 until height) {
145 | var startT: Option[Int] = None
146 | var startB: Option[Int] = None
147 |
148 | def flushT(start: Int, end: Int): Unit = {
149 | val length = (end - start + 1) * Map.roomSize
150 | val cenX = start * Map.roomSize + (end - start + 1) * Map.roomHalfSize
151 | val cenY = Map.roomSize * y
152 | tWalls += new ContinuousWall(new Vector2f(cenX, cenY), length)
153 | }
154 | def flushB(start: Int, end: Int): Unit = {
155 | val length = (end - start + 1) * Map.roomSize
156 | val cenX = start * Map.roomSize + (end - start + 1) * Map.roomHalfSize
157 | val cenY = Map.roomSize * (y + 1)
158 | bWalls += new ContinuousWall(new Vector2f(cenX, cenY), length)
159 | }
160 |
161 | for (x <- 0 until width) {
162 | val hasWallT = roomAt(x, y).map(hasTWall).getOrElse(false)
163 | (startT, hasWallT) match {
164 | case (Some(s), true) => // nothing to do, keep going
165 | case (None, true) => startT = Some(x) // start of a new wall
166 | case (Some(s), false) =>
167 | flushT(s, x - 1)
168 | startT = None // End of the wall
169 | case (None, false) => // nothing to do, keep going
170 | }
171 |
172 | val hasWallB = roomAt(x, y).map(hasBWall).getOrElse(false)
173 | (startB, hasWallB) match {
174 | case (Some(s), true) => // nothing to do, keep going
175 | case (None, true) => startB = Some(x) // start of a new wall
176 | case (Some(s), false) =>
177 | flushB(s, x - 1)
178 | startB = None // End of the wall
179 | case (None, false) => // nothing to do, keep going
180 | }
181 | }
182 |
183 | for (s <- startT) {
184 | flushT(s, width - 1)
185 | }
186 | for (s <- startB) {
187 | flushB(s, width - 1)
188 | }
189 | }
190 |
191 | for (x <- 0 until width) {
192 | var startL: Option[Int] = None
193 | var startR: Option[Int] = None
194 |
195 | def flushL(start: Int, end: Int): Unit = {
196 | val length = (end - start + 1) * Map.roomSize
197 | val cenY = start * Map.roomSize + (end - start + 1) * Map.roomHalfSize
198 | val cenX = Map.roomSize * x
199 | lWalls += new ContinuousWall(new Vector2f(cenX, cenY), length)
200 | }
201 | def flushR(start: Int, end: Int): Unit = {
202 | val length = (end - start + 1) * Map.roomSize
203 | val cenY = start * Map.roomSize + (end - start + 1) * Map.roomHalfSize
204 | val cenX = Map.roomSize * (x + 1)
205 | rWalls += new ContinuousWall(new Vector2f(cenX, cenY), length)
206 | }
207 |
208 | for (y <- 0 until height) {
209 | val hasWallL = roomAt(x, y).map(hasLWall).getOrElse(false)
210 | (startL, hasWallL) match {
211 | case (Some(s), true) => // nothing to do, keep going
212 | case (None, true) => startL = Some(y) // start of a new wall
213 | case (Some(s), false) =>
214 | flushL(s, y - 1)
215 | startL = None // End of the wall
216 | case (None, false) => // nothing to do, keep going
217 | }
218 |
219 | val hasWallR = roomAt(x, y).map(hasRWall).getOrElse(false)
220 | (startR, hasWallR) match {
221 | case (Some(s), true) => // nothing to do, keep going
222 | case (None, true) => startR = Some(y) // start of a new wall
223 | case (Some(s), false) =>
224 | flushR(s, y - 1)
225 | startR = None // End of the wall
226 | case (None, false) => // nothing to do, keep going
227 | }
228 | }
229 |
230 | for (s <- startL) {
231 | flushL(s, height - 1)
232 | }
233 | for (s <- startR) {
234 | flushR(s, height - 1)
235 | }
236 | }
237 |
238 | (lWalls.toArray, rWalls.toArray, tWalls.toArray, bWalls.toArray)
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/demo/Misc.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | import scala.concurrent.{ Future, ExecutionContext }
4 | import games.{ Utils, Resource }
5 | import games.math._
6 | import games.input.{ Key, Keyboard }
7 |
8 | import scala.collection.immutable
9 |
10 | object Misc {
11 | def loadConfigFile(resourceConfig: Resource)(implicit ec: ExecutionContext): Future[immutable.Map[String, String]] = {
12 | val configFileFuture = Utils.getTextDataFromResource(resourceConfig)
13 |
14 | for (configFile <- configFileFuture) yield {
15 | val lines = configFile.lines
16 | lines.map { line =>
17 | val tokens = line.split("=", 2)
18 | if (tokens.size != 2) throw new RuntimeException("Config file malformed: \"" + line + "\"")
19 | val key = tokens(0)
20 | val value = tokens(1)
21 |
22 | (key, value)
23 | }.toMap
24 | }
25 | }
26 |
27 | def conv(v: network.Vector2): Vector2f = new Vector2f(v.x, v.y)
28 | def conv(v: Vector2f): network.Vector2 = network.Vector2(v.x, v.y)
29 | def conv(v: network.State): State = v match {
30 | case network.Absent => Absent
31 | case network.Present(uPosition, uVelocity, uOrientation, uHealth) => new Present(conv(uPosition), conv(uVelocity), uOrientation, uHealth)
32 | }
33 | def conv(v: State): network.State = v match {
34 | case Absent => network.Absent
35 | case x: Present => network.Present(conv(x.position), conv(x.velocity), x.orientation, x.health)
36 | }
37 | }
38 |
39 | sealed trait KeyLayout {
40 | val forward: Key
41 | val backward: Key
42 | val left: Key
43 | val right: Key
44 |
45 | val mouseLock: Key
46 | val fullscreen: Key
47 | val renderingMode: Key
48 | val escape: Key
49 | val changeLayout: Key
50 |
51 | val volumeIncrease: Key
52 | val volumeDecrease: Key
53 | }
54 |
55 | object Qwerty extends KeyLayout {
56 | final val forward: Key = Key.W
57 | final val backward: Key = Key.S
58 | final val left: Key = Key.A
59 | final val right: Key = Key.D
60 |
61 | final val mouseLock: Key = Key.L
62 | final val fullscreen: Key = Key.F
63 | final val renderingMode: Key = Key.M
64 | final val escape: Key = Key.Escape
65 | final val changeLayout: Key = Key.Tab
66 |
67 | final val volumeIncrease: Key = Key.NumAdd
68 | final val volumeDecrease: Key = Key.NumSubstract
69 | }
70 |
71 | object Azerty extends KeyLayout {
72 | final val forward: Key = Key.Z
73 | final val backward: Key = Key.S
74 | final val left: Key = Key.Q
75 | final val right: Key = Key.D
76 |
77 | final val mouseLock: Key = Key.L
78 | final val fullscreen: Key = Key.F
79 | final val renderingMode: Key = Key.M
80 | final val escape: Key = Key.Escape
81 | final val changeLayout: Key = Key.Tab
82 |
83 | final val volumeIncrease: Key = Key.NumAdd
84 | final val volumeDecrease: Key = Key.NumSubstract
85 | }
86 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/demo/Physics.scala:
--------------------------------------------------------------------------------
1 | package games.demo
2 |
3 | import games.math._
4 |
5 | import scala.collection.{ immutable, mutable }
6 |
7 | object Physics {
8 | final val playerRadius: Float = 0.5f
9 | var projectileVelocity: Float = _
10 |
11 | def load(config: immutable.Map[String, String]): Unit = {
12 | this.projectileVelocity = config.get("projectileVelocity").map(_.toFloat).getOrElse(15f) // default: velocity of 15
13 | }
14 |
15 | /**
16 | * Sets an angle in degrees in the interval ]-180, 180]
17 | */
18 | def angleCentered(angle: Float): Float = {
19 | var ret = angle
20 | while (ret > 180f) ret -= 360f
21 | while (ret <= -180f) ret += 360f
22 | ret
23 | }
24 |
25 | /**
26 | * Sets an angle in degrees in the interval [0, 360[
27 | */
28 | def anglePositive(angle: Float): Float = {
29 | var ret = angle
30 | while (ret >= 360f) ret -= 360f
31 | while (ret < 0f) ret += 360f
32 | ret
33 | }
34 |
35 | def interpol(curIn: Float, minIn: Float, maxIn: Float, startValue: Float, endValue: Float): Float = startValue + (curIn - minIn) * (endValue - startValue) / (maxIn - minIn)
36 |
37 | var map: Map = _
38 |
39 | def setupMap(map: Map): Unit = {
40 | this.map = map
41 | }
42 |
43 | def projectileStep(proj: (Int, Projectile), players: immutable.Map[Int, Present], elapsedSinceLastFrame: Float): Int = {
44 | val (shooterId, projectile) = proj
45 |
46 | // Move the projectile
47 | val direction = Matrix2f.rotate2D(-projectile.orientation) * new Vector2f(0, -1)
48 | val distance = projectileVelocity * elapsedSinceLastFrame
49 |
50 | val startPoint = projectile.position
51 |
52 | // Collision detection
53 |
54 | // players
55 | val playerRes = players.toSeq.flatMap { p =>
56 | val (playerId, player) = p
57 | if (shooterId != playerId) { // No self-hit...
58 | // From http://mathworld.wolfram.com/Circle-LineIntersection.html
59 |
60 | val x1 = startPoint.x - player.position.x
61 | val y1 = startPoint.y - player.position.y
62 |
63 | val r = playerRadius
64 | val r_square = r * r
65 |
66 | if ((x1 * x1 + y1 * y1) <= r_square) { // Already in contact
67 | Some((playerId, 0f))
68 | } else {
69 | val dx = direction.x
70 | val dy = direction.y
71 |
72 | val x2 = x1 + dx
73 | val y2 = y1 + dy
74 |
75 | // dr is always 1 (dx and dy are part of a unit vector)
76 | val d = x1 * y2 - x2 * y1
77 |
78 | val disc = r_square - d * d
79 |
80 | if (disc >= 0f) {
81 | // I know Math.signum looks the same, but we need sgn(0) to return 1 in this case
82 | def sgn(in: Float): Float = if (in < 0f) -1f else 1f
83 |
84 | val disc_sqrt = Math.sqrt(disc).toFloat
85 |
86 | val partx = sgn(dy) * dx * disc_sqrt
87 | val party = Math.abs(dy) * disc_sqrt
88 |
89 | // First contact point
90 | val cx1 = (d * dy + partx)
91 | val cy1 = (-d * dx + party)
92 |
93 | // Second contact point
94 | val cx2 = (d * dy - partx)
95 | val cy2 = (-d * dx - party)
96 |
97 | // Use dot product to compute distance from initial point
98 | val l1 = (cx1 - x1) * dx + (cy1 - y1) * dy
99 | val l2 = (cx2 - x1) * dx + (cy2 - y1) * dy
100 |
101 | // Check which one(s) is(are) really reached during this step
102 | val l1_valid = (l1 >= 0f && l1 <= distance)
103 | val l2_valid = (l2 >= 0f && l2 <= distance)
104 |
105 | if (l1_valid || l2_valid) {
106 | val collision_distance = if (l1_valid && l2_valid) Math.min(l1, l2) else if (l1_valid) l1 else l2
107 | Some((playerId, collision_distance))
108 | } else None
109 | } else None
110 | }
111 | } else None
112 | }
113 |
114 | // Map
115 | val hWalls = map.ctWalls ++ map.cbWalls
116 | val vWalls = map.crWalls ++ map.clWalls
117 |
118 | val hRes = hWalls.flatMap { hWall =>
119 | val dx = direction.x
120 | val dy = direction.y
121 |
122 | val wx = hWall.position.x
123 | val wy = hWall.position.y
124 |
125 | val x4 = startPoint.x
126 | val y4 = startPoint.y
127 |
128 | val x3 = x4 + dx
129 | val y3 = y4 + dy
130 |
131 | if (dy == 0f) None // Parallel to the wall, no contact
132 | else {
133 | val x = (wy * dx - x3 * y4 + x4 * y3) / dy
134 | val y = wy
135 |
136 | val l = (x - x4) * dx + (y - y4) * dy
137 | val l_valid = (l >= 0f && l <= distance) && Math.abs(wx - x) < hWall.halfLength
138 |
139 | if (l_valid) Some((0, l))
140 | else None
141 | }
142 | }
143 |
144 | val vRes = vWalls.flatMap { vWall =>
145 | val dx = direction.x
146 | val dy = direction.y
147 |
148 | val wx = vWall.position.x
149 | val wy = vWall.position.y
150 |
151 | val x4 = startPoint.x
152 | val y4 = startPoint.y
153 |
154 | val x3 = x4 + dx
155 | val y3 = y4 + dy
156 |
157 | if (dx == 0f) None // Parallel to the wall, no contact
158 | else {
159 | val y = (wx * dy - y3 * x4 + y4 * x3) / dx
160 | val x = wx
161 |
162 | val l = (x - x4) * dx + (y - y4) * dy
163 | val l_valid = (l >= 0f && l <= distance) && Math.abs(wy - y) < vWall.halfLength
164 |
165 | if (l_valid) Some((0, l))
166 | else None
167 | }
168 | }
169 |
170 | val res = playerRes ++ hRes ++ vRes
171 |
172 | val (playerId, distance_travel) = if (res.isEmpty) (-1, distance) // No collision
173 | else res.reduce { (a1, a2) => // Collision(s) detected, take the closest one
174 | val (p1, d1) = a1
175 | val (p2, d2) = a2
176 |
177 | if (d1 < d2) a1
178 | else a2
179 | }
180 |
181 | projectile.position = projectile.position + direction * distance_travel // new position
182 | playerId // -1 no collision, 0 wall, > 0 player hit
183 | }
184 |
185 | def playerStep(player: Present, elapsedSinceLastFrame: Float): Unit = {
186 | // Move the player
187 | player.position += (Matrix2f.rotate2D(-player.orientation) * player.velocity) * elapsedSinceLastFrame
188 |
189 | // Collision with the map
190 | val playerPos = player.position
191 |
192 | for (wall <- map.ctWalls) {
193 | val pos = wall.position
194 | val length = wall.length
195 | val halfLength = wall.halfLength
196 | if (Math.abs(pos.y - playerPos.y) < playerRadius && Math.abs(pos.x - playerPos.x) < (playerRadius + halfLength)) { // AABB test
197 | if (Math.abs(pos.x - playerPos.x) < halfLength) { // front contact
198 | playerPos.y = pos.y + playerRadius
199 | } else { // contact on the corner
200 | val cornerPos = if (playerPos.x > pos.x) { // Right corner
201 | pos + new Vector2f(halfLength, 0)
202 | } else { // Left corner
203 | pos + new Vector2f(-halfLength, 0)
204 | }
205 | val diff = (playerPos - cornerPos)
206 | if (diff.length() < playerRadius) {
207 | diff.normalize()
208 | diff *= playerRadius
209 | Vector2f.set(cornerPos + diff, playerPos)
210 | }
211 | }
212 | }
213 | }
214 |
215 | for (wall <- map.cbWalls) {
216 | val pos = wall.position
217 | val length = wall.length
218 | val halfLength = wall.halfLength
219 | if (Math.abs(pos.y - playerPos.y) < playerRadius && Math.abs(pos.x - playerPos.x) < (playerRadius + halfLength)) { // AABB test
220 | if (Math.abs(pos.x - playerPos.x) < halfLength) { // front contact
221 | playerPos.y = pos.y - playerRadius
222 | } else { // contact on the corner
223 | val cornerPos = if (playerPos.x > pos.x) { // Right corner
224 | pos + new Vector2f(halfLength, 0)
225 | } else { // Left corner
226 | pos + new Vector2f(-halfLength, 0)
227 | }
228 | val diff = (playerPos - cornerPos)
229 | if (diff.length() < playerRadius) {
230 | diff.normalize()
231 | diff *= playerRadius
232 | Vector2f.set(cornerPos + diff, playerPos)
233 | }
234 | }
235 | }
236 | }
237 |
238 | for (wall <- map.clWalls) {
239 | val pos = wall.position
240 | val length = wall.length
241 | val halfLength = wall.halfLength
242 | if (Math.abs(pos.x - playerPos.x) < playerRadius && Math.abs(pos.y - playerPos.y) < (playerRadius + halfLength)) { // AABB test
243 | if (Math.abs(pos.y - playerPos.y) < halfLength) { // front contact
244 | playerPos.x = pos.x + playerRadius
245 | } else { // contact on the corner
246 | val cornerPos = if (playerPos.y > pos.y) { // down corner
247 | pos + new Vector2f(0, halfLength)
248 | } else { // up corner
249 | pos + new Vector2f(0, -halfLength)
250 | }
251 | val diff = (playerPos - cornerPos)
252 | if (diff.length() < playerRadius) {
253 | diff.normalize()
254 | diff *= playerRadius
255 | Vector2f.set(cornerPos + diff, playerPos)
256 | }
257 | }
258 | }
259 | }
260 |
261 | for (wall <- map.crWalls) {
262 | val pos = wall.position
263 | val length = wall.length
264 | val halfLength = wall.halfLength
265 | if (Math.abs(pos.x - playerPos.x) < playerRadius && Math.abs(pos.y - playerPos.y) < (playerRadius + halfLength)) { // AABB test
266 | if (Math.abs(pos.y - playerPos.y) < halfLength) { // front contact
267 | playerPos.x = pos.x - playerRadius
268 | } else { // contact on the corner
269 | val cornerPos = if (playerPos.y > pos.y) { // down corner
270 | pos + new Vector2f(0, halfLength)
271 | } else { // up corner
272 | pos + new Vector2f(0, -halfLength)
273 | }
274 | val diff = (playerPos - cornerPos)
275 | if (diff.length() < playerRadius) {
276 | diff.normalize()
277 | diff *= playerRadius
278 | Vector2f.set(cornerPos + diff, playerPos)
279 | }
280 | }
281 | }
282 | }
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/input/Accelerometer.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import java.io.Closeable
4 |
5 | abstract class Accelerometer extends Closeable {
6 | def current(): Option[games.math.Vector3f]
7 |
8 | def close(): Unit = {}
9 | }
10 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/input/Input.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | case class Position(x: Int, y: Int)
4 |
5 | private[games] class BiMap[R, T](entries: (R, T)*) {
6 | private val map = entries.toMap
7 | private val reverseMap = entries.map { case (a, b) => (b, a) }.toMap
8 |
9 | def getForLocal(loc: R): Option[T] = map.get(loc)
10 | def getForRemote(rem: T): Option[R] = reverseMap.get(rem)
11 | }
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/input/Keyboard.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import java.io.Closeable
4 |
5 | object Keyboard {
6 | private[games]type KeyMapper[T] = BiMap[Key, T]
7 | }
8 |
9 | object `package` {
10 | type Key = Int
11 | }
12 |
13 | object Key {
14 | final val Space: Key = 1
15 | final val Apostrophe: Key = 2
16 | final val Circumflex: Key = 3
17 | final val Comma: Key = 4
18 | final val Period: Key = 5
19 | final val Minus: Key = 6
20 | final val Slash: Key = 7
21 | final val SemiColon: Key = 8
22 | final val Equal: Key = 9
23 | final val BracketLeft: Key = 10
24 | final val BracketRight: Key = 11
25 | final val BackSlash: Key = 12
26 | final val GraveAccent: Key = 13
27 | final val Escape: Key = 14
28 | final val Enter: Key = 15
29 | final val Tab: Key = 16
30 | final val BackSpace: Key = 17
31 | final val Insert: Key = 18
32 | final val Delete: Key = 19
33 | final val Right: Key = 20
34 | final val Left: Key = 21
35 | final val Down: Key = 22
36 | final val Up: Key = 23
37 | final val PageUp: Key = 24
38 | final val PageDown: Key = 25
39 | final val Home: Key = 26
40 | final val End: Key = 27
41 | final val CapsLock: Key = 28
42 | final val ScrollLock: Key = 29
43 | final val NumLock: Key = 30
44 | final val PrintScreen: Key = 31
45 | final val Pause: Key = 32
46 | final val N0: Key = 100
47 | final val N1: Key = 101
48 | final val N2: Key = 102
49 | final val N3: Key = 103
50 | final val N4: Key = 104
51 | final val N5: Key = 105
52 | final val N6: Key = 106
53 | final val N7: Key = 107
54 | final val N8: Key = 108
55 | final val N9: Key = 109
56 | final val A: Key = 200
57 | final val B: Key = 201
58 | final val C: Key = 202
59 | final val D: Key = 203
60 | final val E: Key = 204
61 | final val F: Key = 205
62 | final val G: Key = 206
63 | final val H: Key = 207
64 | final val I: Key = 208
65 | final val J: Key = 209
66 | final val K: Key = 210
67 | final val L: Key = 211
68 | final val M: Key = 212
69 | final val N: Key = 213
70 | final val O: Key = 214
71 | final val P: Key = 215
72 | final val Q: Key = 216
73 | final val R: Key = 217
74 | final val S: Key = 218
75 | final val T: Key = 219
76 | final val U: Key = 220
77 | final val V: Key = 221
78 | final val W: Key = 222
79 | final val X: Key = 223
80 | final val Y: Key = 224
81 | final val Z: Key = 225
82 | final val F1: Key = 300
83 | final val F2: Key = 301
84 | final val F3: Key = 302
85 | final val F4: Key = 303
86 | final val F5: Key = 304
87 | final val F6: Key = 305
88 | final val F7: Key = 306
89 | final val F8: Key = 307
90 | final val F9: Key = 308
91 | final val F10: Key = 309
92 | final val F11: Key = 310
93 | final val F12: Key = 311
94 | final val F13: Key = 312
95 | final val F14: Key = 313
96 | final val F15: Key = 314
97 | final val F16: Key = 315
98 | final val F17: Key = 316
99 | final val F18: Key = 317
100 | final val F19: Key = 318
101 | final val F20: Key = 319
102 | final val F21: Key = 320
103 | final val F22: Key = 321
104 | final val F23: Key = 322
105 | final val F24: Key = 323
106 | final val F25: Key = 324
107 | final val Num0: Key = 400
108 | final val Num1: Key = 401
109 | final val Num2: Key = 402
110 | final val Num3: Key = 403
111 | final val Num4: Key = 404
112 | final val Num5: Key = 405
113 | final val Num6: Key = 406
114 | final val Num7: Key = 407
115 | final val Num8: Key = 408
116 | final val Num9: Key = 409
117 | final val NumDecimal: Key = 410
118 | final val NumDivide: Key = 411
119 | final val NumMultiply: Key = 412
120 | final val NumSubstract: Key = 413
121 | final val NumAdd: Key = 414
122 | final val NumEnter: Key = 415
123 | final val NumEqual: Key = 416
124 | final val ShiftLeft: Key = 500
125 | final val ShiftRight: Key = 501
126 | final val ControlLeft: Key = 502
127 | final val ControlRight: Key = 503
128 | final val AltLeft: Key = 504
129 | final val AltRight: Key = 505
130 | final val SuperLeft: Key = 506
131 | final val SuperRight: Key = 507
132 | final val MenuLeft: Key = 508
133 | final val MenuRight: Key = 509
134 | }
135 |
136 | case class KeyboardEvent(key: Key, down: Boolean)
137 |
138 | abstract class Keyboard extends Closeable {
139 | def isKeyDown(key: Key): Boolean
140 | def nextEvent(): Option[KeyboardEvent]
141 |
142 | def close(): Unit = {}
143 | }
144 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/input/Mouse.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import java.io.Closeable
4 |
5 | object Mouse {
6 | private[games]type ButtonMapper[T] = BiMap[Button, T]
7 | }
8 |
9 | sealed abstract class Button
10 |
11 | object Button {
12 | case object Left extends Button
13 | case object Right extends Button
14 | case object Middle extends Button
15 | case class Aux(num: Int) extends Button
16 | }
17 |
18 | sealed abstract class Wheel
19 |
20 | object Wheel {
21 | case object Up extends Wheel
22 | case object Down extends Wheel
23 | case object Left extends Wheel
24 | case object Right extends Wheel
25 | }
26 |
27 | abstract sealed class MouseEvent
28 | case class ButtonEvent(button: Button, down: Boolean) extends MouseEvent
29 | case class WheelEvent(direction: Wheel) extends MouseEvent
30 |
31 | abstract class Mouse extends Closeable {
32 | def position: Position
33 | def deltaMotion: Position
34 |
35 | def locked: Boolean
36 | def locked_=(locked: Boolean): Unit
37 |
38 | def isButtonDown(button: Button): Boolean
39 | def nextEvent(): Option[MouseEvent]
40 |
41 | def isInside(): Boolean
42 |
43 | def close(): Unit = {}
44 | }
45 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/input/Touch.scala:
--------------------------------------------------------------------------------
1 | package games.input
2 |
3 | import java.io.Closeable
4 |
5 | case class Touch(identifier: Int, position: Position)
6 |
7 | case class TouchEvent(data: Touch, start: Boolean)
8 |
9 | abstract class Touchscreen extends Closeable {
10 | def touches: Seq[Touch]
11 |
12 | def nextEvent(): Option[TouchEvent]
13 |
14 | def close(): Unit = {}
15 | }
16 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/MajorOrder.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | abstract sealed class MajorOrder
4 |
5 | case object RowMajor extends MajorOrder {
6 | override def toString = "Row-major"
7 | }
8 |
9 | case object ColumnMajor extends MajorOrder {
10 | override def toString = "Column-major"
11 | }
12 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Matrix.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import java.nio.FloatBuffer
4 |
5 | abstract class Matrix {
6 | def apply(row: Int, col: Int): Float
7 | def update(row: Int, col: Int, v: Float): Unit
8 |
9 | def load(src: FloatBuffer, order: MajorOrder): Matrix
10 | def store(dst: FloatBuffer, order: MajorOrder): Matrix
11 |
12 | def setIdentity(): Matrix
13 | def setZero(): Matrix
14 |
15 | def invert(): Matrix
16 | def invertedCopy(): Matrix
17 |
18 | def negate(): Matrix
19 | def negatedCopy(): Matrix
20 |
21 | def transpose(): Matrix
22 | def transposedCopy(): Matrix
23 |
24 | def determinant(): Float
25 |
26 | def copy(): Matrix
27 | }
28 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Matrix2f.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import java.nio.FloatBuffer
4 |
5 | /**
6 | * Ported from LWJGL source code
7 | */
8 | class Matrix2f extends Matrix {
9 | private[math] var m00, m11: Float = 1
10 | private[math] var m01, m10: Float = 0
11 |
12 | def this(a00: Float, a01: Float, a10: Float, a11: Float) = {
13 | this()
14 | // Internally stored as Column-major
15 | m00 = a00
16 | m01 = a10
17 | m10 = a01
18 | m11 = a11
19 | }
20 |
21 | def this(col0: Vector2f, col1: Vector2f) = {
22 | this()
23 |
24 | m00 = col0.x
25 | m01 = col0.y
26 |
27 | m10 = col1.x
28 | m11 = col1.y
29 | }
30 |
31 | def this(m: Matrix2f) = {
32 | this()
33 | Matrix2f.set(m, this)
34 | }
35 |
36 | def apply(row: Int, col: Int): Float = (row, col) match {
37 | case (0, 0) => m00
38 | case (0, 1) => m10
39 | case (1, 0) => m01
40 | case (1, 1) => m11
41 | case _ => throw new IndexOutOfBoundsException
42 | }
43 |
44 | def update(row: Int, col: Int, v: Float): Unit = (row, col) match {
45 | case (0, 0) => m00 = v
46 | case (0, 1) => m10 = v
47 | case (1, 0) => m01 = v
48 | case (1, 1) => m11 = v
49 | case _ => throw new IndexOutOfBoundsException
50 | }
51 |
52 | def load(src: FloatBuffer, order: MajorOrder): Matrix2f = order match {
53 | case RowMajor =>
54 | m00 = src.get()
55 | m10 = src.get()
56 | m01 = src.get()
57 | m11 = src.get()
58 | this
59 | case ColumnMajor =>
60 | m00 = src.get()
61 | m01 = src.get()
62 | m10 = src.get()
63 | m11 = src.get()
64 | this
65 | }
66 | def store(dst: FloatBuffer, order: MajorOrder): Matrix2f = order match {
67 | case RowMajor =>
68 | dst.put(m00)
69 | dst.put(m10)
70 | dst.put(m01)
71 | dst.put(m11)
72 | this
73 | case ColumnMajor =>
74 | dst.put(m00)
75 | dst.put(m01)
76 | dst.put(m10)
77 | dst.put(m11)
78 | this
79 | }
80 |
81 | def setIdentity(): Matrix2f = {
82 | m00 = 1
83 | m01 = 0
84 | m10 = 0
85 | m11 = 1
86 | this
87 | }
88 | def setZero(): Matrix2f = {
89 | m00 = 0
90 | m01 = 0
91 | m10 = 0
92 | m11 = 0
93 | this
94 | }
95 |
96 | def column(colIdx: Int): Vector2f = {
97 | val ret = new Vector2f
98 | Matrix2f.getColumn(this, colIdx, ret)
99 | ret
100 | }
101 | def row(rowIdx: Int): Vector2f = {
102 | val ret = new Vector2f
103 | Matrix2f.getRow(this, rowIdx, ret)
104 | ret
105 | }
106 |
107 | def invert(): Matrix2f = {
108 | Matrix2f.invert(this, this)
109 | this
110 | }
111 | def invertedCopy(): Matrix2f = {
112 | val ret = new Matrix2f
113 | Matrix2f.invert(this, ret)
114 | ret
115 | }
116 |
117 | def negate(): Matrix2f = {
118 | Matrix2f.negate(this, this)
119 | this
120 | }
121 | def negatedCopy(): Matrix2f = {
122 | val ret = new Matrix2f
123 | Matrix2f.negate(this, ret)
124 | ret
125 | }
126 |
127 | def transpose(): Matrix2f = {
128 | Matrix2f.transpose(this, this)
129 | this
130 | }
131 | def transposedCopy(): Matrix2f = {
132 | val ret = new Matrix2f
133 | Matrix2f.transpose(this, ret)
134 | ret
135 | }
136 |
137 | def determinant(): Float = {
138 | m00 * m11 - m01 * m10
139 | }
140 |
141 | def copy(): Matrix2f = {
142 | val ret = new Matrix2f
143 | Matrix2f.set(this, ret)
144 | ret
145 | }
146 |
147 | def +(m: Matrix2f): Matrix2f = {
148 | val ret = new Matrix2f
149 | Matrix2f.add(this, m, ret)
150 | ret
151 | }
152 |
153 | def +=(m: Matrix2f): Unit = {
154 | Matrix2f.add(this, m, this)
155 | }
156 |
157 | def -(m: Matrix2f): Matrix2f = {
158 | val ret = new Matrix2f
159 | Matrix2f.sub(this, m, ret)
160 | ret
161 | }
162 |
163 | def -=(m: Matrix2f): Unit = {
164 | Matrix2f.sub(this, m, this)
165 | }
166 |
167 | def *(m: Matrix2f): Matrix2f = {
168 | val ret = new Matrix2f
169 | Matrix2f.mult(this, m, ret)
170 | ret
171 | }
172 |
173 | def *=(m: Matrix2f): Unit = {
174 | Matrix2f.mult(this, m, this)
175 | }
176 |
177 | def *(v: Float): Matrix2f = {
178 | val ret = new Matrix2f
179 | Matrix2f.mult(this, v, ret)
180 | ret
181 | }
182 |
183 | def *=(v: Float): Unit = {
184 | Matrix2f.mult(this, v, this)
185 | }
186 |
187 | def /(v: Float): Matrix2f = {
188 | val ret = new Matrix2f
189 | Matrix2f.div(this, v, ret)
190 | ret
191 | }
192 |
193 | def /=(v: Float): Unit = {
194 | Matrix2f.div(this, v, this)
195 | }
196 |
197 | def *(v: Vector2f): Vector2f = {
198 | val ret = new Vector2f
199 | Matrix2f.mult(this, v, ret)
200 | ret
201 | }
202 |
203 | def transform(v: Vector2f): Vector2f = {
204 | val ret = new Vector2f
205 | Matrix2f.mult(this, v, ret)
206 | ret
207 | }
208 |
209 | def toHomogeneous(): Matrix3f = {
210 | val ret = new Matrix3f
211 | Matrix2f.setHomogeneous(this, ret)
212 | ret
213 | }
214 |
215 | override def toString: String = {
216 | var sb = ""
217 | sb += m00 + " " + m10 + "\n"
218 | sb += m01 + " " + m11 + "\n"
219 | sb
220 | }
221 |
222 | override def equals(obj: Any): Boolean = {
223 | if (obj == null) false
224 | if (!obj.isInstanceOf[Matrix2f]) false
225 |
226 | val o = obj.asInstanceOf[Matrix2f]
227 |
228 | m00 == o.m00 &&
229 | m01 == o.m01 &&
230 | m10 == o.m10 &&
231 | m11 == o.m11
232 | }
233 |
234 | override def hashCode(): Int = {
235 | m00.hashCode ^ m01.hashCode ^ m10.hashCode ^ m11.hashCode
236 | }
237 | }
238 |
239 | object Matrix2f {
240 | def set(src: Matrix2f, dst: Matrix2f): Unit = {
241 | dst.m00 = src.m00
242 | dst.m01 = src.m01
243 | dst.m10 = src.m10
244 | dst.m11 = src.m11
245 | }
246 |
247 | def getColumn(src: Matrix2f, colIdx: Int, dst: Vector2f): Unit = colIdx match {
248 | case 0 =>
249 | dst.x = src.m00
250 | dst.y = src.m01
251 |
252 | case 1 =>
253 | dst.x = src.m10
254 | dst.y = src.m11
255 |
256 | case _ => throw new IndexOutOfBoundsException
257 | }
258 |
259 | def getRow(src: Matrix2f, rowIdx: Int, dst: Vector2f): Unit = rowIdx match {
260 | case 0 =>
261 | dst.x = src.m00
262 | dst.y = src.m10
263 |
264 | case 1 =>
265 | dst.x = src.m01
266 | dst.y = src.m11
267 |
268 | case _ => throw new IndexOutOfBoundsException
269 | }
270 |
271 | def setColumn(src: Vector2f, dst: Matrix2f, colIdx: Int): Unit = colIdx match {
272 | case 0 =>
273 | dst.m00 = src.x
274 | dst.m01 = src.y
275 |
276 | case 1 =>
277 | dst.m10 = src.x
278 | dst.m11 = src.y
279 |
280 | case _ => throw new IndexOutOfBoundsException
281 | }
282 | def setRow(src: Vector2f, dst: Matrix2f, rowIdx: Int): Unit = rowIdx match {
283 | case 0 =>
284 | dst.m00 = src.x
285 | dst.m10 = src.y
286 |
287 | case 1 =>
288 | dst.m01 = src.x
289 | dst.m11 = src.y
290 |
291 | case _ => throw new IndexOutOfBoundsException
292 | }
293 |
294 | def setHomogeneous(src: Matrix2f, dst: Matrix3f): Unit = {
295 | dst.m00 = src.m00
296 | dst.m01 = src.m01
297 | dst.m02 = 0f
298 |
299 | dst.m10 = src.m10
300 | dst.m11 = src.m11
301 | dst.m12 = 0f
302 |
303 | dst.m20 = 0f
304 | dst.m21 = 0f
305 | dst.m22 = 1f
306 | }
307 |
308 | def negate(src: Matrix2f, dst: Matrix2f): Unit = {
309 | dst.m00 = -src.m00
310 | dst.m01 = -src.m01
311 | dst.m10 = -src.m10
312 | dst.m11 = -src.m11
313 | }
314 |
315 | def invert(src: Matrix2f, dst: Matrix2f): Unit = {
316 | val det = src.determinant
317 |
318 | if (det != 0) {
319 | val det_inv = 1f / det
320 |
321 | val t00 = src.m11 * det_inv
322 | val t01 = -src.m01 * det_inv
323 | val t11 = src.m00 * det_inv
324 | val t10 = -src.m10 * det_inv
325 |
326 | dst.m00 = t00
327 | dst.m01 = t01
328 | dst.m10 = t10
329 | dst.m11 = t11
330 | }
331 | }
332 |
333 | def transpose(src: Matrix2f, dst: Matrix2f): Unit = {
334 | val t10 = src.m10
335 | val t01 = src.m01
336 |
337 | dst.m00 = src.m00
338 | dst.m01 = t10
339 | dst.m10 = t01
340 | dst.m11 = src.m11
341 | }
342 |
343 | def add(m1: Matrix2f, m2: Matrix2f, dst: Matrix2f): Unit = {
344 | dst.m00 = m1.m00 + m2.m00
345 | dst.m01 = m1.m10 + m2.m10
346 | dst.m10 = m1.m01 + m2.m01
347 | dst.m11 = m1.m11 + m2.m11
348 | }
349 |
350 | def sub(m1: Matrix2f, m2: Matrix2f, dst: Matrix2f): Unit = {
351 | dst.m00 = m1.m00 - m2.m00
352 | dst.m01 = m1.m10 - m2.m10
353 | dst.m10 = m1.m01 - m2.m01
354 | dst.m11 = m1.m11 - m2.m11
355 | }
356 |
357 | def mult(left: Matrix2f, right: Matrix2f, dst: Matrix2f): Unit = {
358 | val m00 = left.m00 * right.m00 + left.m10 * right.m01
359 | val m01 = left.m01 * right.m00 + left.m11 * right.m01
360 | val m10 = left.m00 * right.m10 + left.m10 * right.m11
361 | val m11 = left.m01 * right.m10 + left.m11 * right.m11
362 |
363 | dst.m00 = m00
364 | dst.m01 = m01
365 | dst.m10 = m10
366 | dst.m11 = m11
367 | }
368 |
369 | def mult(left: Matrix2f, right: Vector2f, dst: Vector2f): Unit = {
370 | val x = left.m00 * right.x + left.m10 * right.y
371 | val y = left.m01 * right.x + left.m11 * right.y
372 |
373 | dst.x = x
374 | dst.y = y
375 | }
376 |
377 | def mult(left: Matrix2f, right: Float, dst: Matrix2f): Unit = {
378 | dst.m00 = left.m00 * right
379 | dst.m01 = left.m01 * right
380 | dst.m10 = left.m10 * right
381 | dst.m11 = left.m11 * right
382 | }
383 |
384 | def div(left: Matrix2f, right: Float, dst: Matrix2f): Unit = {
385 | dst.m00 = left.m00 / right
386 | dst.m01 = left.m01 / right
387 | dst.m10 = left.m10 / right
388 | dst.m11 = left.m11 / right
389 | }
390 |
391 | /**
392 | * Generates the non-homogeneous rotation matrix for a given angle (in degrees) around the origin
393 | */
394 | def rotate2D(angle: Float): Matrix2f = {
395 | val ret = new Matrix2f
396 | setRotate2D(angle, ret)
397 | ret
398 | }
399 |
400 | def setRotate2D(angle: Float, dst: Matrix2f): Unit = {
401 | val radAngle = Math.toRadians(angle)
402 |
403 | val c = Math.cos(radAngle).toFloat
404 | val s = Math.sin(radAngle).toFloat
405 |
406 | dst.m00 = c
407 | dst.m10 = -s
408 |
409 | dst.m01 = s
410 | dst.m11 = c
411 | }
412 |
413 | /**
414 | * Generates the non-homogeneous scaling matrix for a given scale vector around the origin
415 | */
416 | def scale2D(scale: Vector2f): Matrix2f = {
417 | val ret = new Matrix2f
418 | setScale2D(scale, ret)
419 | ret
420 | }
421 |
422 | def setScale2D(scale: Vector2f, dst: Matrix2f): Unit = {
423 | dst.m00 = scale.x
424 | dst.m10 = 0f
425 |
426 | dst.m01 = 0f
427 | dst.m11 = scale.y
428 | }
429 | }
430 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/MatrixStack.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import scala.collection.mutable.ArrayBuffer
4 |
5 | class MatrixStack[T <: Matrix](var current: T) {
6 | private val stack: ArrayBuffer[T] = new ArrayBuffer[T]()
7 |
8 | def push: Unit = {
9 | stack += current.copy.asInstanceOf[T]
10 | }
11 |
12 | def pop: T = if (empty) {
13 | throw new RuntimeException("Stack empty")
14 | } else {
15 | stack.remove(stack.size - 1)
16 | }
17 |
18 | def empty: Boolean = stack.size == 0
19 | }
20 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Utils.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | object Utils {
4 | def cotan(v: Double): Double = {
5 | 1.0 / Math.tan(v)
6 | }
7 |
8 | /**
9 | * Orthogonalize an existing 3x3 matrix.
10 | * Can be used to make sure a matrix meant to be orthogonal stays orthogonal
11 | * despite floating-point rounding errors (e.g. a matrix used to accumulate
12 | * a lot of rotations)
13 | */
14 | def orthogonalize(mat: Matrix3f): Unit = {
15 | // Maybe a better way here: http://stackoverflow.com/questions/23080791/eigen-re-orthogonalization-of-rotation-matrix
16 | val r1 = mat.column(0)
17 | val r2 = mat.column(1)
18 | val r3 = mat.column(2)
19 |
20 | r1.normalize()
21 |
22 | val newR2 = r2 - r1 * (r1 * r2)
23 | newR2.normalize()
24 |
25 | val newR3 = r1.cross(newR2)
26 | newR3.normalize()
27 |
28 | Matrix3f.setColumn(r1, mat, 0)
29 | Matrix3f.setColumn(newR2, mat, 1)
30 | Matrix3f.setColumn(newR3, mat, 2)
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Vector.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import java.nio.FloatBuffer
4 |
5 | abstract class Vector {
6 | def apply(pos: Int): Float
7 | def update(pos: Int, v: Float): Unit
8 |
9 | def load(src: FloatBuffer): Vector
10 | def store(dst: FloatBuffer): Vector
11 |
12 | def normalize(): Vector
13 | def normalizedCopy(): Vector
14 |
15 | def negate(): Vector
16 | def negatedCopy(): Vector
17 |
18 | def lengthSquared(): Float
19 | def length(): Float
20 |
21 | def copy(): Vector
22 | }
23 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Vector2f.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import java.nio.FloatBuffer
4 |
5 | class Vector2f extends Vector {
6 | var x, y: Float = _
7 |
8 | def this(v1: Float, v2: Float) = {
9 | this()
10 | x = v1
11 | y = v2
12 | }
13 |
14 | def this(v: Vector2f) = {
15 | this()
16 | Vector2f.set(v, this)
17 | }
18 |
19 | def apply(pos: Int): Float = pos match {
20 | case 0 => x
21 | case 1 => y
22 | case _ => throw new IndexOutOfBoundsException
23 | }
24 |
25 | def update(pos: Int, v: Float): Unit = pos match {
26 | case 0 => x = v
27 | case 1 => y = v
28 | case _ => throw new IndexOutOfBoundsException
29 | }
30 |
31 | def load(src: FloatBuffer): Vector2f = {
32 | x = src.get
33 | y = src.get
34 | this
35 | }
36 | def store(dst: FloatBuffer): Vector2f = {
37 | dst.put(x)
38 | dst.put(y)
39 | this
40 | }
41 |
42 | def normalize(): Vector2f = {
43 | val l = length
44 | this /= l
45 | this
46 | }
47 |
48 | def normalizedCopy(): Vector2f = {
49 | val l = length
50 | this / l
51 | }
52 |
53 | def negate(): Vector2f = {
54 | Vector2f.negate(this, this)
55 | this
56 | }
57 |
58 | def negatedCopy(): Vector2f = {
59 | val ret = new Vector2f
60 | Vector2f.negate(this, ret)
61 | ret
62 | }
63 |
64 | def lengthSquared(): Float = {
65 | x * x + y * y
66 | }
67 | def length(): Float = {
68 | Math.sqrt(this.lengthSquared).toFloat
69 | }
70 |
71 | def copy(): Vector2f = {
72 | val ret = new Vector2f
73 | Vector2f.set(this, ret)
74 | ret
75 | }
76 |
77 | def +(v: Vector2f): Vector2f = {
78 | val ret = new Vector2f
79 | Vector2f.add(this, v, ret)
80 | ret
81 | }
82 |
83 | def -(v: Vector2f): Vector2f = {
84 | val ret = new Vector2f
85 | Vector2f.sub(this, v, ret)
86 | ret
87 | }
88 |
89 | def *(v: Vector2f): Float = {
90 | Vector2f.dot(this, v)
91 | }
92 |
93 | def dot(v: Vector2f): Float = {
94 | Vector2f.dot(this, v)
95 | }
96 |
97 | def *(v: Float): Vector2f = {
98 | val ret = new Vector2f
99 | Vector2f.mult(this, v, ret)
100 | ret
101 | }
102 |
103 | def /(v: Float): Vector2f = {
104 | val ret = new Vector2f
105 | Vector2f.div(this, v, ret)
106 | ret
107 | }
108 |
109 | def +=(v: Vector2f): Unit = {
110 | Vector2f.add(this, v, this)
111 | }
112 |
113 | def -=(v: Vector2f): Unit = {
114 | Vector2f.sub(this, v, this)
115 | }
116 |
117 | def *=(v: Float): Unit = {
118 | Vector2f.mult(this, v, this)
119 | }
120 |
121 | def /=(v: Float): Unit = {
122 | Vector2f.div(this, v, this)
123 | }
124 |
125 | def toHomogeneous(): Vector3f = {
126 | val ret = new Vector3f
127 | Vector2f.setHomogeneous(this, ret)
128 | ret
129 | }
130 |
131 | override def toString = {
132 | "Vector2f[" + x + ", " + y + "]"
133 | }
134 |
135 | override def equals(obj: Any): Boolean = {
136 | if (obj == null) false
137 | if (!obj.isInstanceOf[Vector2f]) false
138 |
139 | val o = obj.asInstanceOf[Vector2f]
140 |
141 | x == o.x &&
142 | y == o.y
143 | }
144 |
145 | override def hashCode(): Int = {
146 | x.hashCode ^
147 | y.hashCode
148 | }
149 | }
150 |
151 | object Vector2f {
152 | def set(src: Vector2f, dst: Vector2f): Unit = {
153 | dst.x = src.x
154 | dst.y = src.y
155 | }
156 |
157 | def setHomogeneous(src: Vector2f, dst: Vector3f): Unit = {
158 | dst.x = src.x
159 | dst.y = src.y
160 | dst.z = 1f
161 | }
162 |
163 | def negate(v1: Vector2f, dst: Vector2f): Unit = {
164 | dst.x = -v1.x
165 | dst.y = -v1.y
166 | }
167 |
168 | def add(v1: Vector2f, v2: Vector2f, dst: Vector2f): Unit = {
169 | dst.x = v1.x + v2.x
170 | dst.y = v1.y + v2.y
171 | }
172 |
173 | def sub(v1: Vector2f, v2: Vector2f, dst: Vector2f): Unit = {
174 | dst.x = v1.x - v2.x
175 | dst.y = v1.y - v2.y
176 | }
177 |
178 | def dot(v1: Vector2f, v2: Vector2f): Float = {
179 | v1.x * v2.x + v1.y * v2.y
180 | }
181 |
182 | def mult(v1: Vector2f, v: Float, dst: Vector2f): Unit = {
183 | dst.x = v1.x * v
184 | dst.y = v1.y * v
185 | }
186 |
187 | def div(v1: Vector2f, v: Float, dst: Vector2f): Unit = {
188 | dst.x = v1.x / v
189 | dst.y = v1.y / v
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Vector3f.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import java.nio.FloatBuffer
4 |
5 | class Vector3f extends Vector {
6 | var x, y, z: Float = _
7 |
8 | def this(v1: Float, v2: Float, v3: Float) = {
9 | this()
10 | x = v1
11 | y = v2
12 | z = v3
13 | }
14 |
15 | def this(v: Vector3f) = {
16 | this()
17 | Vector3f.set(v, this)
18 | }
19 |
20 | def apply(pos: Int): Float = pos match {
21 | case 0 => x
22 | case 1 => y
23 | case 2 => z
24 | case _ => throw new IndexOutOfBoundsException
25 | }
26 |
27 | def update(pos: Int, v: Float): Unit = pos match {
28 | case 0 => x = v
29 | case 1 => y = v
30 | case 2 => z = v
31 | case _ => throw new IndexOutOfBoundsException
32 | }
33 |
34 | def load(src: FloatBuffer): Vector3f = {
35 | x = src.get
36 | y = src.get
37 | z = src.get
38 | this
39 | }
40 | def store(dst: FloatBuffer): Vector3f = {
41 | dst.put(x)
42 | dst.put(y)
43 | dst.put(z)
44 | this
45 | }
46 |
47 | def normalize(): Vector3f = {
48 | val l = length
49 | this /= l
50 | this
51 | }
52 |
53 | def normalizedCopy(): Vector3f = {
54 | val l = length
55 | this / l
56 | }
57 |
58 | def negate(): Vector3f = {
59 | Vector3f.negate(this, this)
60 | this
61 | }
62 |
63 | def negatedCopy(): Vector3f = {
64 | val ret = new Vector3f
65 | Vector3f.negate(this, ret)
66 | ret
67 | }
68 |
69 | def lengthSquared(): Float = {
70 | x * x + y * y + z * z
71 | }
72 | def length(): Float = {
73 | Math.sqrt(this.lengthSquared).toFloat
74 | }
75 |
76 | def copy(): Vector3f = {
77 | val ret = new Vector3f
78 | Vector3f.set(this, ret)
79 | ret
80 | }
81 |
82 | def +(v: Vector3f): Vector3f = {
83 | val ret = new Vector3f
84 | Vector3f.add(this, v, ret)
85 | ret
86 | }
87 |
88 | def -(v: Vector3f): Vector3f = {
89 | val ret = new Vector3f
90 | Vector3f.sub(this, v, ret)
91 | ret
92 | }
93 |
94 | def *(v: Vector3f): Float = {
95 | Vector3f.dot(this, v)
96 | }
97 |
98 | def dot(v: Vector3f): Float = {
99 | Vector3f.dot(this, v)
100 | }
101 |
102 | def cross(v: Vector3f): Vector3f = {
103 | val ret = new Vector3f
104 | Vector3f.cross(this, v, ret)
105 | ret
106 | }
107 |
108 | def x(v: Vector3f): Vector3f = {
109 | val ret = new Vector3f
110 | Vector3f.cross(this, v, ret)
111 | ret
112 | }
113 |
114 | def `x=`(v: Vector3f): Vector3f = {
115 | Vector3f.cross(this, v, this)
116 | this
117 | }
118 |
119 | def *(v: Float): Vector3f = {
120 | val ret = new Vector3f
121 | Vector3f.mult(this, v, ret)
122 | ret
123 | }
124 |
125 | def /(v: Float): Vector3f = {
126 | val ret = new Vector3f
127 | Vector3f.div(this, v, ret)
128 | ret
129 | }
130 |
131 | def +=(v: Vector3f): Unit = {
132 | Vector3f.add(this, v, this)
133 | }
134 |
135 | def -=(v: Vector3f): Unit = {
136 | Vector3f.sub(this, v, this)
137 | }
138 |
139 | def *=(v: Float): Unit = {
140 | Vector3f.mult(this, v, this)
141 | }
142 |
143 | def /=(v: Float): Unit = {
144 | Vector3f.div(this, v, this)
145 | }
146 |
147 | def toCartesian(): Vector2f = {
148 | val ret = new Vector2f
149 | Vector3f.setCartesian(this, ret)
150 | ret
151 | }
152 |
153 | def toHomogeneous(): Vector4f = {
154 | val ret = new Vector4f
155 | Vector3f.setHomogeneous(this, ret)
156 | ret
157 | }
158 |
159 | override def toString = {
160 | "Vector3f[" + x + ", " + y + ", " + z + "]"
161 | }
162 |
163 | override def equals(obj: Any): Boolean = {
164 | if (obj == null) false
165 | if (!obj.isInstanceOf[Vector3f]) false
166 |
167 | val o = obj.asInstanceOf[Vector3f]
168 |
169 | x == o.x &&
170 | y == o.y &&
171 | z == o.z
172 | }
173 |
174 | override def hashCode(): Int = {
175 | x.hashCode ^
176 | y.hashCode ^
177 | z.hashCode
178 | }
179 | }
180 |
181 | object Vector3f {
182 | def set(src: Vector3f, dst: Vector3f): Unit = {
183 | dst.x = src.x
184 | dst.y = src.y
185 | dst.z = src.z
186 | }
187 |
188 | def setCartesian(src: Vector3f, dst: Vector2f): Unit = {
189 | dst.x = src.x
190 | dst.y = src.y
191 | }
192 |
193 | def setHomogeneous(src: Vector3f, dst: Vector4f): Unit = {
194 | dst.x = src.x
195 | dst.y = src.y
196 | dst.z = src.z
197 | dst.w = 1f
198 | }
199 |
200 | def negate(v1: Vector3f, dst: Vector3f): Unit = {
201 | dst.x = -v1.x
202 | dst.y = -v1.y
203 | dst.z = -v1.z
204 | }
205 |
206 | def add(v1: Vector3f, v2: Vector3f, dst: Vector3f): Unit = {
207 | dst.x = v1.x + v2.x
208 | dst.y = v1.y + v2.y
209 | dst.z = v1.z + v2.z
210 | }
211 |
212 | def sub(v1: Vector3f, v2: Vector3f, dst: Vector3f): Unit = {
213 | dst.x = v1.x - v2.x
214 | dst.y = v1.y - v2.y
215 | dst.z = v1.z - v2.z
216 | }
217 |
218 | def dot(v1: Vector3f, v2: Vector3f): Float = {
219 | v1.x * v2.x + v1.y * v2.y + v1.z * v2.z
220 | }
221 |
222 | def cross(left: Vector3f, right: Vector3f, dst: Vector3f): Unit = {
223 | val x = left.y * right.z - left.z * right.y
224 | val y = right.x * left.z - right.z * left.x
225 | val z = left.x * right.y - left.y * right.x
226 |
227 | dst.x = x
228 | dst.y = y
229 | dst.z = z
230 | }
231 |
232 | def mult(v1: Vector3f, v: Float, dst: Vector3f): Unit = {
233 | dst.x = v1.x * v
234 | dst.y = v1.y * v
235 | dst.z = v1.z * v
236 | }
237 |
238 | def div(v1: Vector3f, v: Float, dst: Vector3f): Unit = {
239 | dst.x = v1.x / v
240 | dst.y = v1.y / v
241 | dst.z = v1.z / v
242 | }
243 |
244 | def Right = new Vector3f(1, 0, 0)
245 | def Up = new Vector3f(0, 1, 0)
246 | def Back = new Vector3f(0, 0, 1)
247 | def Left = new Vector3f(-1, 0, 0)
248 | def Down = new Vector3f(0, -1, 0)
249 | def Front = new Vector3f(0, 0, -1)
250 | }
251 |
--------------------------------------------------------------------------------
/demo/shared/src/main/scala/games/math/Vector4f.scala:
--------------------------------------------------------------------------------
1 | package games.math
2 |
3 | import java.nio.FloatBuffer
4 |
5 | class Vector4f extends Vector {
6 | var x, y, z, w: Float = _
7 |
8 | def this(v1: Float, v2: Float, v3: Float, v4: Float) = {
9 | this()
10 | x = v1
11 | y = v2
12 | z = v3
13 | w = v4
14 | }
15 |
16 | def this(v: Vector4f) = {
17 | this()
18 | Vector4f.set(v, this)
19 | }
20 |
21 | def apply(pos: Int): Float = pos match {
22 | case 0 => x
23 | case 1 => y
24 | case 2 => z
25 | case 3 => w
26 | case _ => throw new IndexOutOfBoundsException
27 | }
28 |
29 | def update(pos: Int, v: Float): Unit = pos match {
30 | case 0 => x = v
31 | case 1 => y = v
32 | case 2 => z = v
33 | case 3 => w = v
34 | case _ => throw new IndexOutOfBoundsException
35 | }
36 |
37 | def load(src: FloatBuffer): Vector4f = {
38 | x = src.get
39 | y = src.get
40 | z = src.get
41 | w = src.get
42 | this
43 | }
44 | def store(dst: FloatBuffer): Vector4f = {
45 | dst.put(x)
46 | dst.put(y)
47 | dst.put(z)
48 | dst.put(w)
49 | this
50 | }
51 |
52 | def normalize(): Vector4f = {
53 | val l = length
54 | this /= l
55 | this
56 | }
57 |
58 | def normalizedCopy(): Vector4f = {
59 | val l = length
60 | this / l
61 | }
62 |
63 | def negate(): Vector4f = {
64 | Vector4f.negate(this, this)
65 | this
66 | }
67 |
68 | def negatedCopy(): Vector4f = {
69 | val ret = new Vector4f
70 | Vector4f.negate(this, ret)
71 | ret
72 | }
73 |
74 | def lengthSquared(): Float = {
75 | x * x + y * y + z * z + w * w
76 | }
77 | def length(): Float = {
78 | Math.sqrt(this.lengthSquared).toFloat
79 | }
80 |
81 | def copy(): Vector4f = {
82 | val ret = new Vector4f
83 | Vector4f.set(this, ret)
84 | ret
85 | }
86 |
87 | def +(v: Vector4f): Vector4f = {
88 | val ret = new Vector4f
89 | Vector4f.add(this, v, ret)
90 | ret
91 | }
92 |
93 | def -(v: Vector4f): Vector4f = {
94 | val ret = new Vector4f
95 | Vector4f.sub(this, v, ret)
96 | ret
97 | }
98 |
99 | def *(v: Vector4f): Float = {
100 | Vector4f.dot(this, v)
101 | }
102 |
103 | def dot(v: Vector4f): Float = {
104 | Vector4f.dot(this, v)
105 | }
106 |
107 | def *(v: Float): Vector4f = {
108 | val ret = new Vector4f
109 | Vector4f.mult(this, v, ret)
110 | ret
111 | }
112 |
113 | def /(v: Float): Vector4f = {
114 | val ret = new Vector4f
115 | Vector4f.div(this, v, ret)
116 | ret
117 | }
118 |
119 | def +=(v: Vector4f): Unit = {
120 | Vector4f.add(this, v, this)
121 | }
122 |
123 | def -=(v: Vector4f): Unit = {
124 | Vector4f.sub(this, v, this)
125 | }
126 |
127 | def *=(v: Float): Unit = {
128 | Vector4f.mult(this, v, this)
129 | }
130 |
131 | def /=(v: Float): Unit = {
132 | Vector4f.div(this, v, this)
133 | }
134 |
135 | def toCartesian(): Vector3f = {
136 | val ret = new Vector3f
137 | Vector4f.setCartesian(this, ret)
138 | ret
139 | }
140 |
141 | override def toString = {
142 | "Vector4f[" + x + ", " + y + ", " + z + ", " + w + "]"
143 | }
144 |
145 | override def equals(obj: Any): Boolean = {
146 | if (obj == null) false
147 | if (!obj.isInstanceOf[Vector4f]) false
148 |
149 | val o = obj.asInstanceOf[Vector4f]
150 |
151 | x == o.x &&
152 | y == o.y &&
153 | z == o.z &&
154 | w == o.w
155 | }
156 |
157 | override def hashCode(): Int = {
158 | x.hashCode ^
159 | y.hashCode ^
160 | z.hashCode ^
161 | w.hashCode
162 | }
163 | }
164 |
165 | object Vector4f {
166 | def set(src: Vector4f, dst: Vector4f): Unit = {
167 | dst.x = src.x
168 | dst.y = src.y
169 | dst.z = src.z
170 | dst.w = src.w
171 | }
172 |
173 | def setCartesian(src: Vector4f, dst: Vector3f): Unit = {
174 | dst.x = src.x
175 | dst.y = src.y
176 | dst.z = src.z
177 | }
178 |
179 | def negate(v1: Vector4f, dst: Vector4f): Unit = {
180 | dst.x = -v1.x
181 | dst.y = -v1.y
182 | dst.z = -v1.z
183 | dst.w = -v1.w
184 | }
185 |
186 | def add(v1: Vector4f, v2: Vector4f, dst: Vector4f): Unit = {
187 | dst.x = v1.x + v2.x
188 | dst.y = v1.y + v2.y
189 | dst.z = v1.z + v2.z
190 | dst.w = v1.w + v2.w
191 | }
192 |
193 | def sub(v1: Vector4f, v2: Vector4f, dst: Vector4f): Unit = {
194 | dst.x = v1.x - v2.x
195 | dst.y = v1.y - v2.y
196 | dst.z = v1.z - v2.z
197 | dst.w = v1.w - v2.w
198 | }
199 |
200 | def dot(v1: Vector4f, v2: Vector4f): Float = {
201 | v1.x * v2.x + v1.y * v2.y + v1.z * v2.z + v1.w * v2.w
202 | }
203 |
204 | def mult(v1: Vector4f, v: Float, dst: Vector4f): Unit = {
205 | dst.x = v1.x * v
206 | dst.y = v1.y * v
207 | dst.z = v1.z * v
208 | dst.w = v1.w * v
209 | }
210 |
211 | def div(v1: Vector4f, v: Float, dst: Vector4f): Unit = {
212 | dst.x = v1.x / v
213 | dst.y = v1.y / v
214 | dst.z = v1.z / v
215 | dst.w = v1.w / v
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/demoJS-launcher/index-fastopt.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Demo for Scala.js-games
7 |
8 |
9 | Demo for Scala.js-games - fast-optimized version
10 |
11 | After having compiled and optimized properly the code for the application (using `serverDemoJS/reStart` from SBT), you should see the demo on the current page (fully-optimized version available here).
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/demoJS-launcher/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Demo for Scala.js-games
7 |
8 |
9 | Demo for Scala.js-games - full-optimized version
10 |
11 | After having compiled and optimized properly the code for the application (using `serverDemoJS/reStart` from SBT), you should see the demo on the current page (fast-optimized version available here).
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=0.13.7
2 |
--------------------------------------------------------------------------------
/project/plugin.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") /* For the demoJS project */
2 |
3 | addSbtPlugin("com.github.philcali" % "sbt-lwjgl-plugin" % "3.1.5") /* For the demoJVM project */
4 |
5 | //addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.3.16") /* For the demoAndroid project */
6 |
7 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") /* Local server for the demoJS project */
8 |
--------------------------------------------------------------------------------