├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── controls.png ├── demo ├── android │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── games │ │ │ └── demoAndroid │ │ │ ├── HelloJni.java │ │ │ └── PrecompiledJni.java │ │ ├── jni │ │ ├── Android.mk │ │ ├── Application.mk │ │ └── hello-jni.c │ │ ├── libs │ │ ├── arm64-v8a │ │ │ └── libprecompiled-jni.so │ │ ├── armeabi-v7a │ │ │ └── libprecompiled-jni.so │ │ ├── armeabi │ │ │ └── libprecompiled-jni.so │ │ ├── mips │ │ │ └── libprecompiled-jni.so │ │ ├── mips64 │ │ │ └── libprecompiled-jni.so │ │ ├── x86 │ │ │ └── libprecompiled-jni.so │ │ └── x86_64 │ │ │ └── libprecompiled-jni.so │ │ └── scala │ │ └── games │ │ ├── demo │ │ └── Specifics.scala │ │ └── demoAndroid │ │ └── Launcher.scala ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── games │ │ ├── Utils.scala │ │ ├── audio │ │ ├── Context.scala │ │ └── Data.scala │ │ ├── demo │ │ └── Specifics.scala │ │ ├── demoJS │ │ └── Launcher.scala │ │ ├── input │ │ ├── Accelerometer.scala │ │ ├── Keyboard.scala │ │ ├── Mouse.scala │ │ └── Touch.scala │ │ └── opengl │ │ └── GLES2.scala ├── jvm │ └── src │ │ └── main │ │ └── scala │ │ └── games │ │ ├── Utils.scala │ │ ├── audio │ │ ├── Context.scala │ │ ├── Converter.scala │ │ ├── Data.scala │ │ └── VorbisDecoder.scala │ │ ├── demo │ │ └── Specifics.scala │ │ ├── demoJVM │ │ └── Launcher.scala │ │ ├── input │ │ ├── Keyboard.scala │ │ └── Mouse.scala │ │ └── opengl │ │ └── GLES2.scala ├── server │ └── src │ │ └── main │ │ └── scala │ │ └── games │ │ └── demo │ │ └── server │ │ ├── Boot.scala │ │ └── Service.scala ├── shared-server │ └── src │ │ └── main │ │ └── scala │ │ └── games │ │ └── demo │ │ └── network │ │ └── Message.scala └── shared │ └── src │ └── main │ ├── resources │ └── games │ │ └── demo │ │ ├── config │ │ ├── maps │ │ └── map1 │ │ ├── models │ │ ├── bullet │ │ │ ├── bullet.mtl │ │ │ ├── bullet.obj │ │ │ └── main │ │ ├── character │ │ │ ├── character.mtl │ │ │ ├── character.obj │ │ │ └── main │ │ ├── floor │ │ │ ├── floor.mtl │ │ │ ├── floor.obj │ │ │ └── main │ │ ├── list │ │ └── wall │ │ │ ├── main │ │ │ ├── wall.mtl │ │ │ └── wall.obj │ │ ├── shaders │ │ ├── list │ │ ├── simple2d │ │ │ ├── fragment.c │ │ │ └── vertex.c │ │ └── simple3d │ │ │ ├── fragment.c │ │ │ └── vertex.c │ │ └── sounds │ │ ├── flashkit │ │ ├── NOTICE.md │ │ ├── Sniper_R-MelonHea-7518_hifi.ogg │ │ └── Spark_1-kayden_r-8968_hifi.ogg │ │ ├── test_mono.ogg │ │ └── test_stereo.ogg │ └── scala │ └── games │ ├── Resource.scala │ ├── Utils.scala │ ├── audio │ └── Context.scala │ ├── demo │ ├── Data.scala │ ├── Engine.scala │ ├── Map.scala │ ├── Misc.scala │ ├── Physics.scala │ └── Rendering.scala │ ├── input │ ├── Accelerometer.scala │ ├── Input.scala │ ├── Keyboard.scala │ ├── Mouse.scala │ └── Touch.scala │ ├── math │ ├── MajorOrder.scala │ ├── Matrix.scala │ ├── Matrix2f.scala │ ├── Matrix3f.scala │ ├── Matrix4f.scala │ ├── MatrixStack.scala │ ├── Utils.scala │ ├── Vector.scala │ ├── Vector2f.scala │ ├── Vector3f.scala │ └── Vector4f.scala │ ├── opengl │ ├── GLES2.scala │ └── GLES2Debug.scala │ └── utils │ └── SimpleOBJParser.scala ├── demoJS-launcher ├── index-fastopt.html └── index.html └── project ├── build.properties └── plugin.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | bin/ 11 | lib_managed/ 12 | src_managed/ 13 | project/boot/ 14 | project/plugins/project/ 15 | 16 | # Scala-IDE specific 17 | .scala_dependencies 18 | .worksheet 19 | 20 | .classpath 21 | .project 22 | .settings/ 23 | 24 | .cache 25 | *.sjsir 26 | 27 | # benchmarking the Scala.js incremental optimizer: https://groups.google.com/forum/#!topic/scala-js/E_nSW7MF-nE 28 | sjs-measurements-*.log 29 | sjsincoptbench.sbt 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Joël Rossier 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of scalajs-games nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalajs-games 2 | 3 | Library for graphics/audio/inputs for Scala.js 4 | 5 | ## Demo 6 | 7 | ### Commands 8 | 9 | The mouse-keyboard controls are the well-known WASD: 10 | * Maintain **W** to go forward, **S** to go backward, **A** to move left, **D** to move right (or the arrows) 11 | * Left mouse button to shoot and mouse movement to change the orientation 12 | * Press **Escape** to exit 13 | * Press **F** to toggle fullscreen 14 | * Press **L** to toggle pointer lock 15 | * Press **M** to change rendering mode (polygon/wireframe) 16 | * Press **Tab** to alternate between Qwerty and Azerty key-mapping. 17 | * Press **numpad +** to increase the audio volume 18 | * Press **numpad -** to decrease the audio volume 19 | 20 | The touchscreen controls are: 21 | * Left part of the image to move 22 | * Right part of the image to change the orientation 23 | * Tap the image to shoot 24 | * Tap the top-left corner of the image to toggle fullscreen 25 | * Tap the top-right corner of the image to switch the part to move and the part for orientation (may be more comfortable if you are left handed) 26 | ![Touchscreen controls](/controls.png) 27 | 28 | Players are dispatched in room of up to 8 players. 29 | 30 | ### Launching 31 | 32 | #### General 33 | 34 | The address the clients will attempt to reach is located in the file ```demo/shared/src/main/resources/games/demo/config```. The responsible line is ```server=ws://localhost:8080/``` by default, which should be fine if you are starting both the server and the client locally on the same machine, but if you are planning to connect to the server from other machines, you should replace ```localhost``` by something more reachable (your IP or your domain name if you have one). 35 | 36 | #### Server + Scala.js client 37 | 38 | * Run ```sbt```. Once in SBT, enter ```serverDemoJS/reStart```. This will start the server and make the Scala.js client available through it (press Ctrl + C to stop the server and exit SBT). 39 | * To use the Scala.js client, open your browser (preferably Chrome or Firefox) to the specified address ([http://localhost:8080/](http://localhost:8080/) by default). 40 | 41 | #### JVM client (requires a running server to connect to) 42 | 43 | Run ```sbt demoJVM/run```. 44 | 45 | ## License 46 | 47 | Scalajs-games code itself is under the BSD license 48 | 49 | The dependencies for the JVM code are: 50 | * [LWJGL](https://github.com/LWJGL/lwjgl) 51 | * [JOrbis](http://www.jcraft.com/jorbis/) 52 | 53 | The dependencies for the Scala.js code are: 54 | * [Aurora.js](https://github.com/audiocogs/aurora.js) (optional) 55 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | //import android.Keys._ 2 | //import android.Dependencies.aar 3 | 4 | lazy val commonSettings = Seq( 5 | version := "0.1-SNAPSHOT", 6 | scalaVersion := "2.11.7", 7 | persistLauncher in Compile := true, 8 | persistLauncher in Test := true, 9 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/", 10 | resolvers += "Spray" at "http://repo.spray.io", 11 | scalacOptions ++= Seq( 12 | "-deprecation", 13 | "-feature" 14 | ), 15 | libraryDependencies ++= Seq( 16 | "com.lihaoyi" %%% "upickle" % "0.2.8" 17 | ) 18 | ) 19 | 20 | lazy val demo = crossProject 21 | .crossType(CrossType.Full) 22 | .in(file("demo")) 23 | 24 | /* Common settings */ 25 | 26 | .settings( 27 | commonSettings: _* 28 | ) 29 | .settings( 30 | testFrameworks += new TestFramework("utest.runner.Framework"), 31 | libraryDependencies ++= Seq( 32 | "com.github.olivierblanvillain" %%% "transport-core" % "0.1-SNAPSHOT", 33 | "com.lihaoyi" %%% "utest" % "0.3.0" % "test" 34 | ), 35 | unmanagedSourceDirectories in Compile += baseDirectory.value / ".." / "shared-server" / "src" / "main" / "scala", 36 | unmanagedSourceDirectories in Test += baseDirectory.value / ".." / "shared-server" / "src" / "test" / "scala" 37 | ) 38 | 39 | /* JavaScript settings */ 40 | 41 | .jsSettings( 42 | name := "demoJS", 43 | skip in packageJSDependencies := false, 44 | libraryDependencies ++= Seq( 45 | "org.scala-js" %%% "scalajs-dom" % "0.8.0", 46 | "com.github.olivierblanvillain" %%% "transport-javascript" % "0.1-SNAPSHOT" 47 | ) 48 | ) 49 | 50 | /* Standard JVM settings */ 51 | 52 | .jvmSettings( 53 | LWJGLPlugin.lwjglSettings ++ Seq( 54 | LWJGLPlugin.lwjgl.version := "2.9.3" 55 | ): _* 56 | ) 57 | .jvmSettings( 58 | name := "demoJVM", 59 | connectInput in run := true, 60 | unmanagedResourceDirectories in Compile += baseDirectory.value / ".." / "shared" / "src" / "main" / "resources", 61 | libraryDependencies ++= Seq( 62 | "com.github.olivierblanvillain" %%% "transport-tyrus" % "0.1-SNAPSHOT", 63 | "org.jcraft" % "jorbis" % "0.0.17" 64 | ) 65 | ) 66 | 67 | lazy val demoJVM = demo.jvm 68 | lazy val demoJS = demo.js 69 | /*lazy val demoAndroid = project 70 | .in(file("demo/android")) 71 | .settings( 72 | commonSettings: _* 73 | ) 74 | .settings( 75 | android.Plugin.androidBuild: _* 76 | ) 77 | .settings( 78 | name := "demoAndroid", 79 | platformTarget in Android := "android-14", 80 | proguardScala in Android := true, 81 | proguardOptions in Android ++= Seq( 82 | "-ignorewarnings", 83 | "-keep class org.glassfish.tyrus.**", // Somehow, tyrus doesn't seem to like proguard 84 | "-keep class scala.Dynamic" // TODO should be removed 85 | ), 86 | unmanagedSourceDirectories in Compile += baseDirectory.value / ".." / "shared" / "src" / "main" / "scala", 87 | unmanagedSourceDirectories in Test += baseDirectory.value / ".." / "shared" / "src" / "test" / "scala", 88 | unmanagedResourceDirectories in Compile += baseDirectory.value / ".." / "shared" / "src" / "main" / "resources", 89 | unmanagedResourceDirectories in Test += baseDirectory.value / ".." / "shared" / "src" / "test" / "resources", 90 | libraryDependencies ++= Seq( 91 | "com.github.olivierblanvillain" %% "transport-core" % "0.1-SNAPSHOT", 92 | "com.github.olivierblanvillain" %% "transport-tyrus" % "0.1-SNAPSHOT", 93 | aar("com.google.android.gms" % "play-services" % "4.0.30"), 94 | aar("com.android.support" % "support-v4" % "r7") 95 | ) 96 | )*/ 97 | 98 | /* Spray server for the demoJS project */ 99 | 100 | lazy val serverDemoJS = project 101 | .in(file("demo/server")) 102 | .settings( 103 | spray.revolver.RevolverPlugin.Revolver.settings: _* 104 | ) 105 | .settings( 106 | commonSettings: _* 107 | ) 108 | .settings( 109 | libraryDependencies ++= { 110 | Seq( 111 | "com.wandoulabs.akka" %% "spray-websocket" % "0.1.4" 112 | ) 113 | }, 114 | unmanagedSourceDirectories in Compile += baseDirectory.value / ".." / "shared-server" / "src" / "main" / "scala", 115 | unmanagedSourceDirectories in Test += baseDirectory.value / ".." / "shared-server" / "src" / "test" / "scala", 116 | (resources in Compile) += (fastOptJS in (demoJS, Compile)).value.data, 117 | (resources in Compile) += (fullOptJS in (demoJS, Compile)).value.data 118 | ) 119 | -------------------------------------------------------------------------------- /controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelross/scalajs-game-test/920cb927838ebdadde538bc618d001924a495431/controls.png -------------------------------------------------------------------------------- /demo/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/android/src/main/java/games/demoAndroid/HelloJni.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package games.demoAndroid; 17 | 18 | 19 | public class HelloJni 20 | { 21 | /* A native method that is implemented by the 22 | * 'hello-jni' native library, which is packaged 23 | * with this application. 24 | */ 25 | public native String stringFromJNI(); 26 | 27 | /* This is another native method declaration that is *not* 28 | * implemented by 'hello-jni'. This is simply to show that 29 | * you can declare as many native methods in your Java code 30 | * as you want, their implementation is searched in the 31 | * currently loaded native libraries only the first time 32 | * you call them. 33 | * 34 | * Trying to call this function will result in a 35 | * java.lang.UnsatisfiedLinkError exception ! 36 | */ 37 | public native String unimplementedStringFromJNI(); 38 | 39 | /* this is used to load the 'hello-jni' library on application 40 | * startup. The library has already been unpacked into 41 | * /data/data/com.example.hellojni/lib/libhello-jni.so at 42 | * installation time by the package manager. 43 | */ 44 | static { 45 | System.loadLibrary("hello-jni"); 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /demo/android/src/main/java/games/demoAndroid/PrecompiledJni.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package games.demoAndroid; 17 | 18 | 19 | public class PrecompiledJni 20 | { 21 | /* A native method that is implemented by the 22 | * 'hello-jni' native library, which is packaged 23 | * with this application. 24 | */ 25 | public native String precompiledStringFromJNI(); 26 | 27 | /* This is another native method declaration that is *not* 28 | * implemented by 'hello-jni'. This is simply to show that 29 | * you can declare as many native methods in your Java code 30 | * as you want, their implementation is searched in the 31 | * currently loaded native libraries only the first time 32 | * you call them. 33 | * 34 | * Trying to call this function will result in a 35 | * java.lang.UnsatisfiedLinkError exception ! 36 | */ 37 | public native String unimplementedStringFromJNI(); 38 | 39 | /* this is used to load the 'hello-jni' library on application 40 | * startup. The library has already been unpacked into 41 | * /data/data/com.example.hellojni/lib/libhello-jni.so at 42 | * installation time by the package manager. 43 | */ 44 | static { 45 | System.loadLibrary("precompiled-jni"); 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /demo/android/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2009 The Android Open Source Project 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | LOCAL_PATH := $(call my-dir) 16 | 17 | include $(CLEAR_VARS) 18 | 19 | LOCAL_MODULE := hello-jni 20 | LOCAL_SRC_FILES := hello-jni.c 21 | 22 | include $(BUILD_SHARED_LIBRARY) 23 | 24 | -------------------------------------------------------------------------------- /demo/android/src/main/jni/Application.mk: -------------------------------------------------------------------------------- 1 | APP_ABI := all 2 | -------------------------------------------------------------------------------- /demo/android/src/main/jni/hello-jni.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | #include 18 | #include 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 | 14 | Your browser doesn't appear to support the HTML5 canvas element. 15 | 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 | 14 | Your browser doesn't appear to support the HTML5 canvas element. 15 | 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 | --------------------------------------------------------------------------------