Scala and Sbt getting started.

Started by Krux, August 03, 2010, 15:45:32

Previous topic - Next topic

Krux

You might know that Java is not the only programming language on the jvm. Scala is there too. The language is great, and performance is exactly as good as Java. But there is absolutely no documentation on how to create a game in Scala out there in the Internet, so I am going to change this.

We build our project with Simple Built Tool. It is a command line based tool, optimized to built Scala projects. But it can also build mixed Scala/Java projects and Java projects. you get it here. There you can also find how you have to install it depending on your operating system. I use SBT instead of an IDE. You can integrate SBT with e.g. IntelliJ, but I can't tell you how good this works.

As soon as you can launch sbt from the command line, create a new folder and name it something like myFirstSbtSclaLwjglProject, open a terminal and navigate into that location and run sbt here.

now you will be asked if you want to create a new project, type y and hit enter. now you have to enter some project specific details, but it doesn't really matter what you type here. just type something.

the lib folder is for all dependent libraries. Sbt searches in all sub folders, so we can simply copy our LWJGL installation here.
in src/main/scala/ we will have all our Scala source code. create a new file main.scala and write the following in it.

import org.lwjgl._
import opengl.{Display,GL11,DisplayMode}
import GL11._
import input._
import math._

object Main{
	val GAME_TITLE = "My Game"
	val FRAMERATE = 60
	val width = 640
	val height = 480

	val player = new Player(0,0,0);

	var finished = false
	var angle = 0.0f
	var rotation = 0.0f

	def main(args:Array[String]){
		var fullscreen = false
		for(arg <- args){
			arg match{
				case "-fullscreen" =>
					fullscreen = true
			}
		}

		init(fullscreen)
		run
	}

	def init(fullscreen:Boolean){

		println("init Display")
		Display.setTitle(GAME_TITLE)
		Display.setFullscreen(fullscreen)
		Display.setVSyncEnabled(true)
		Display.setDisplayMode(new DisplayMode(width,height))
		Display.create

		println("init gl")
		glEnable(GL_DEPTH_TEST);
		glEnable(GL_LIGHTING)
		glEnable(GL_LIGHT0)
		adjustcam
	}

	def adjustcam(){
		val v = Display.getDisplayMode.getWidth.toFloat/Display.getDisplayMode.getHeight.toFloat
		printf("v:%f",v)
		glMatrixMode(GL_PROJECTION)
		glLoadIdentity
		glFrustum(-v,v,-1,1,1,100)
		glMatrixMode(GL_MODELVIEW)
	}

	def cleanup(){
		Display.destroy
	}

	def run(){
		while(!finished){
			Display.update
			
			logic
			render

			Display.sync(FRAMERATE)
		}
	}

	def logic(){
	    // in scala we can locally import all methods from Keyboard.
	    import Keyboard._
	    
		if(isKeyDown(KEY_ESCAPE))
			finished = true
		if(Display.isCloseRequested)
			finished = true

		// rx and rx store our keyboard input as direction  
		var ry = 0
		var rx = 0
		
		// keys are IKJL for up down left right

		if(isKeyDown(KEY_I))
			ry += 1
		if(isKeyDown(KEY_K))
			ry -= 1
		if(isKeyDown(KEY_J))
			rx -= 1
		if(isKeyDown(KEY_L))
			rx += 1
		
		// this makes the direction relative to the camera position
		// it is a simple rotation matrix you may know from linear algebra 
		val ax = rx*cos(-rotation.toRadians)-ry*sin(-rotation.toRadians)
		val ay = rx*sin(-rotation.toRadians)+ry*cos(-rotation.toRadians)
		
		player.x += 0.1f*ax.toFloat
		player.y += 0.1f*ay.toFloat
		
		// this rotates our camera around the center
		angle += 2.0f % 360
		rotation += 0.2f
	}

	def renderGrid(size : Int){
	    // this creates the nice looking background.
		glDisable(GL_LIGHTING)
		glBegin(GL_LINES)
		for(i <- -size to size){
			glVertex2i(i,-size)
			glVertex2i(i, size)
			glVertex2i(-size,i)
			glVertex2i( size,i)
		}
		glEnd
		glEnable(GL_LIGHTING)
	}

	def render(){
		glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glLoadIdentity

		glTranslatef(0,0,-20)
		glRotatef(-70,1,0,0)
		glRotatef(rotation,0,0,1)
		
		glPushMatrix
		player.applyPos
		//rotate the player just for fun
		glRotatef(angle, 0, 0, 1.0f)
		player.draw
		glPopMatrix
		
		//without background, motion is not visible
		// a green grid is nice and retro
		glColor3f(0,1,0)
		renderGrid(10000)
	}
}

class Player(nx:Float,ny:Float,nz:Float){
	var x:Float = nx
	var y:Float = ny
	var z:Float = nz
	
	def applyPos {
	    glTranslatef(x,y,z)
	}

	def draw = {
		glColor3f(1, 0, 0)
		glBegin(GL_TRIANGLE_FAN)
		glNormal3d( 0, 0, 1);	glVertex3d(0,0,0.5)
		glNormal3d(-1,-1, 1);	glVertex2d(-0.5, -0.5)
		glNormal3d( 1,-1, 1);	glVertex2d(0.5, -0.5)
		glNormal3d( 1, 1, 1);	glVertex2d(0.5, 0.5)
		glNormal3d(-1, 1, 1);	glVertex2d(-0.5, 0.5)
		glNormal3d(-1,-1, 1);	glVertex2d(-0.5, -0.5)
		glEnd
	}
}


now back to our sbt command line. write "compile". and you should see, that sbt compiles everything it it should do, but as soon as you type run, your program crashes with a class not found exception. This is because sbt did not find the LWJGL native libraries. To correct this problem you have to modify sbt settings. i did it with the following file in project/build. You can name it however you want to.

import sbt._
import java.io.File

// simple build tool does not find the so libraries automatically, 
// so we need to import them manually 

class LWJGLProject(info: ProjectInfo) extends DefaultProject(info) {
    // to specify new runJVMOptions we need to fork the execution    
    override def fork = Some(new ForkScalaRun {
        val (os, separator) = System.getProperty("os.name").split(" ")(0).toLowerCase match {
            case "linux" => "linux" -> ":"
            case "mac" => "macosx" -> ":"
            case "windows" => "windows" -> ";"
            case "sunos" => "solaris" -> ":"
            case x => x -> ":"
        }
        
        override def runJVMOptions = super.runJVMOptions ++ Seq("-Djava.library.path=" + System.getProperty("java.library.path") + separator + ("lib" / "native" / os))
        
        override def scalaJars = Seq(buildLibraryJar.asFile, buildCompilerJar.asFile)
    })
}


now run should work. I everything worked you will see something like this:



or you can just download my project file, but you still have to download and install sbt before you can start with it. My project file will be big, because everything LWJGL Scala-compiler and Scala-library is in there, too.
sbttest_tar.gz (29 MB)

PS:
Please tell me how this tutorial works.
if you like it, I might create more tutorials in the future. All Scala based, but maybe more specific in topics about game design

Edit:
thanks to darkfrog for improvements.

jurka

Really good tutorial but did you use some IDE ? I cant figure out the compile setup with intellij. Did you got the sbt and IDE working ?

Krux

I have not used any IDE, because I did not find any that worked well enough for me. IntelliJ didn't compile at all. Netbeans worked (after some configuration), but it does not support SBT at all, and compilation in Netbeans itself does take much longer than anywhere else. Eclipse i have not tried that much, but in the beginning it did not work. I ended in using JEdit. It is fast and it just works out of the box. It does not have autocomplete, but scaladoc and javadoc in a browser next to my Editor are good enough for me. Many IDE features like getter setter generation etc are simply not needed anymore in Scala and others only work on java, so I think it is still the best to use a good text editor for Scala development.

But i can say developing in scala is so much cleaner and nicer that Java or even C++, it just makes me happy to look it the sourcede. here is one example:
Vector class
class Vector2f(_x:Float,_y:Float){
  def x=_x
  def y=_y
  def +(that:Vector2f)=new Vector2f(x+that.x,y+that.y)
  def -(that:Vector2f)=new Vector2f(x-that.x,y-that.y)
}

it has constructor all getters +,-,+=,-= and all that in only 6 lines of code. (setters are not set, because Vector2f are immutable)

darkfrog

Here's my SBT project class:

class SgineProject(info: ProjectInfo) extends DefaultProject(info) {
    override def testSourceRoots = super.testSourceRoots +++ ("src" / "example")
    override def runClasspath = super.runClasspath +++ testClasspath +++ ("src" / "main" / "resources") +++ ("src" / "test" / "resources") +++ ("src" / "example" / "resources")
    
    lazy val runExample = task {
        args => runTask(Some(args(0)), runClasspath).dependsOn(testCompile)
    } completeWith(mainSources.getRelativePaths.toSeq.map(_.replace("/", ".").replace(".scala", "")))
    
    override def fork = Some(new ForkScalaRun {
        val (os, separator) = System.getProperty("os.name").split(" ")(0).toLowerCase match {
            case "linux" => "linux" -> ":"
            case "mac" => "macosx" -> ":"
            case "windows" => "windows" -> ";"
            case "sunos" => "solaris" -> ":"
            case x => x -> ":"
        }
        
        override def runJVMOptions = super.runJVMOptions ++ Seq("-Djava.library.path=" + System.getProperty("java.library.path") + separator + ("lib" / "native" / os))
        
        override def scalaJars = Seq(buildLibraryJar.asFile, buildCompilerJar.asFile)
    })
}


In my case I also have source code in src/example for non-unit tests but this will support the proper separators for the different operating systems and uses the proper references for the scala jars.  This is for my 3d engine Sgine (http://www.sgine.org).  We're in need of additional help if anyone is interested in contributing. :)

Krux


limestrael

Thanks for the tuto, but what if you don't use sbt (I don't like it since it re-downloads every single jar for each project, thus making it weigh at least 15Mo)?

How would you run the lwjgl sample (org.lwjgl.test.WindowCreationTest) through scala for instance?
When I try: scala -cp .:/usr/share/java/lwjgl-2.5/jar/{every lwjgl jar}.jar: -Djava.library.path=/usr/share/java/lwjgl-2.5/native/linux org.lwjgl.test.WindowCreationTest

I got the error:
java.lang.UnsatisfiedLinkError: no lwjgl in java.library.path
   at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1681)
   at java.lang.Runtime.loadLibrary0(Runtime.java:840)
   at java.lang.System.loadLibrary(System.java:1047)
   at org.lwjgl.Sys$1.run(Sys.java:72)
   at java.security.AccessController.doPrivileged(Native Method)
   at org.lwjgl.Sys.doLoadLibrary(Sys.java:65)
   at org.lwjgl.Sys.loadLibrary(Sys.java:81)
   at org.lwjgl.Sys.<clinit>(Sys.java:98)
   at org.lwjgl.opengl.Display.<clinit>(Display.java:128)
   at org.lwjgl.test.WindowCreationTest.initialize(WindowCreationTest.java:80)
   at org.lwjgl.test.WindowCreationTest.main(WindowCreationTest.java:284)
   at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
   at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
   at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
   at java.lang.reflect.Method.invoke(Method.java:616)
   at scala.tools.nsc.ObjectRunner$$anonfun$run$1.apply(ObjectRunner.scala:75)
   at scala.tools.nsc.ObjectRunner$.withContextClassLoader(ObjectRunner.scala:49)
   at scala.tools.nsc.ObjectRunner$.run(ObjectRunner.scala:74)
   at scala.tools.nsc.MainGenericRunner$.main(MainGenericRunner.scala:154)
   at scala.tools.nsc.MainGenericRunner.main(MainGenericRunner.scala)

Moreover, scala can't import math._
Any special .jar to add to classpath?

Krux

no i have not tried to use plain scalac.

But it is not true that sbt re-downloads for each project. You can make subprojects, then you should have shared resources. Might be my next tutorial. But who cares about 15mb project folders, video editors have project folders with more than 350Gb. So just ignore it, the final Product will be smaller again.

Chuck

I came across this while digging up SBT tutorials, so I thought I'd engage in a little thread necromancy: do you have a port of your project/build handy as a build.sbt that works with sbt 0.10?

Chuck

Answering my own question here for the sake of teh google in case other lost souls run across this...

There's an LWJGLProject type defined by the lwjgl plugin: https://github.com/philcali/sbt-lwjgl-plugin