13 KiB
Getting Started
You can easily get started with play-clj by creating a new project with Nightcode and choosing the Clojure game option. You'll get three separate projects for desktop, Android, and iOS, all pointing to the same directories for source code and resources. You can build the projects using Nightcode or Leiningen.
Project Structure
After making a game called hello-world, you'll see three sub-folders: android, desktop, and ios. You'll be spending most of your time in the desktop folder, because it's easier to develop your game on your computer and build it for mobile devices later.
Your actual game code will be in the desktop/src-common folder, and all the images and sound files will be in the desktop/resources folder. When you're ready to build an Android and iOS version, they will read from both of these folders, so you don't have to duplicate any files.
Your First Run
This tutorial will assume that you are using Nightcode, but it should be easy to follow without it as well. To kick things off, navigate to the main file at desktop/src-common/hello_world/core.clj by double-clicking each folder in the sidebar on the left. Once you click on core.clj, you should see its contents appear in the editor pane on the right.
As long as the selection in the sidebar is somewhere inside the desktop folder, the build pane at the bottom will apply to that version of your game. So, try clicking Run and wait for your game to appear. If all goes well, you should see a window with nothing but a small label on the bottom left that says "Hello world!".
Game Structure
Let's look at the basic structure of your game. It starts out with a call to defgame, which creates the basic game object for you and contains a single function called :on-create that runs when your game starts. The only thing it does is hand off to your screen, where all the action takes place.
In defscreen, you'll find that a few simple functions are defined: :on-show and :on-render. The first only runs when the screen is first shown, and the second is run every single time your game wants to draw on the screen (which is typically 60 times per second).
There are many other functions you can put inside defscreen, each letting you run code when certain important events happen. For now, we'll stick to the two we started with, because they are the most fundamental, but you can read the documentation to learn about the others.
Entity System
Most games need some way to keep track of all the things displayed within them. We call these things "entities". Normally, we need to remember attributes such as their position, size, and possibly other values like health and damage. In play-clj, these entities are simply maps, so you can store whatever you want inside of them.
Often, games will store these entities in a list, and in their render function they will loop over the list, perform whatever changes are necessary on the entities (such as moving them), and then call a function to render them. This, of course, would not be idiomatic in Clojure, and leads to more complicated software.
In play-clj, the entities list is stored behind the scenes and is given to you in each function within defscreen. It's a normal Clojure list, so you can't directly change it. Instead, you must return a new entities list at the end of each defscreen function, which will then be provided to all other functions when they run.
Loading a Texture
Right now, you're using the play-clj.ui library to display a label. This library is useful for typical UI needs such as a title screen, but not very useful for the game itself. Let's get rid of it for now, and instead use the play-clj.g2d library, which contains the basic functions for 2D games. Try changing the ns declaration to look like this:
(ns hello-world.core
(:require [play-clj.core :refer :all]
[play-clj.g2d :refer :all]))
Now let's find an image to use as a texture in the game. Find one you'd like to use, such as this Clojure logo, and save it to the desktop/resources folder. Next, simply change the line where the label entity is being created, so it creates a texture from that file instead:
(conj entities (texture "clojure.png"))
Size and Position
If you run the code now, you'll see the image in the bottom-left corner. As mentioned, entities such as the one created by texture are simply Clojure maps. By default, our entity will look like this:
{:type :texture
:object #<TextureRegion com.badlogic.gdx.graphics.g2d.TextureRegion@207bfdc3>}
A texture contains the underlying Java object. By default, it will be drawn at the bottom-left corner with the size of the image itself. You can change the position and size by simply using assoc:
(conj entities (assoc (texture "clojure.png")
:x 50 :y 50 :width 100 :height 100))
Input
Let's add a new function at the end of defscreen called :on-key-down, which runs when a key is pressed:
:on-key-down
(fn [screen entities]
)
If takes the same form as the other functions, expecting a new entities list to be returned at the end. The first argument, screen, which we haven't talked about yet, is a Clojure map containing various important values. In the :on-key-down function, it will contain a :keycode which is a number referring to the key which was pressed.
You can reference the LibGDX documentation to see all the possible keys. To get a key's number, just pass the name as a keyword into the key-code function. For example, (key-code :PAGE_DOWN) will return the number associated with that key. You can also write the keyword in a more Clojuresque way, using lower-case with hyphens, like this: (key-code :page-down).
Let's write a conditional statement that prints out which arrow key you pressed. Note that if a defscreen function returns nil, it leaves the entities list unchanged, so the code below won't wipe out the entities list.
:on-key-down
(fn [screen entities]
(cond
(= (:keycode screen) (key-code :dpad-down))
(println "down")
(= (:keycode screen) (key-code :dpad-up))
(println "up")
(= (:keycode screen) (key-code :dpad-right))
(println "right")
(= (:keycode screen) (key-code :dpad-left))
(println "left")))
Now, what about mobile devices? We don't have a keyboard, so let's create an :on-touch-down function:
:on-touch-down
(fn [screen entities]
)
In this case, the screen map will contain an :x and :y for the point on the screen that was touched. We can simply check to see what part of the screen this point is by using game to get the overall game's width and height.
:on-touch-down
(fn [screen entities]
(cond
(> (:y screen) (* (game :height) (/ 2 3)))
(println "down")
(< (:y screen) (/ (game :height) 3))
(println "up")
(> (:x screen) (* (game :width) (/ 2 3)))
(println "right")
(< (:x screen) (/ (game :width) 3))
(println "left")))
Conveniently, the :on-touch-down function also runs when a mouse is clicked on the screen, so we are adding mouse support to the game as well!
Movement
We already know how to change an entity's position, so let's leverage that to make our image move when we hit the keys. Make a new function above defscreen that takes the entity and a keyword, and returns the entity with an updated position:
(defn move
[entity direction]
(case direction
:down (assoc entity :y (dec (:y entity)))
:up (assoc entity :y (inc (:y entity)))
:right (assoc entity :x (inc (:x entity)))
:left (assoc entity :x (dec (:x entity)))
nil))
Now we can update out :on-key-down and :on-touch-down functions to move the entity. Note that we are technically returning a single entity rather than an entities list, but play-clj will turn it back into a list automatically.
:on-key-down
(fn [screen entities]
(cond
(= (:keycode screen) (key-code :dpad-down))
(move (first entities) :down)
(= (:keycode screen) (key-code :dpad-up))
(move (first entities) :up)
(= (:keycode screen) (key-code :dpad-right))
(move (first entities) :right)
(= (:keycode screen) (key-code :dpad-left))
(move (first entities) :left)))
:on-touch-down
(fn [screen entities]
(cond
(> (:y screen) (* (game :height) (/ 2 3)))
(move (first entities) :down)
(< (:y screen) (/ (game :height) 3))
(move (first entities) :up)
(> (:x screen) (* (game :width) (/ 2 3)))
(move (first entities) :right)
(< (:x screen) (/ (game :width) 3))
(move (first entities) :left)))
Camera
You'll notice that when you resize your game's window, the image looks stretched. That's because the game still thinks it's 800x600 pixels in size, so it stretches accordingly. To make your game adjust its ratio for different screen sizes, you need to use a camera.
First, you need to create a camera and add it to the screen map in the :on-show function, like this:
(update! screen :renderer (stage) :camera (orthographic))
Orthographic cameras are for 2D games, so that's what we're using. Now, we need to create a new defscreen function called :on-resize, which will run whenever the screen resizes:
:on-resize
(fn [screen entities]
)
Lastly, you'll need to make either the width or height of the screen a constant value, so the other dimension can adjust to keep a constant ratio. We'll make the screen's height a constant 600 units in size using the height! function, which returns nil so the entities list won't be changed.
:on-resize
(fn [screen entities]
(height! screen 600))
Now, when you resize your game, the image is no longer stretched!
Using the REPL
It is much faster to develop a game while it's running, and that's what the Clojure REPL lets you do. To get started, just hit the Run with REPL button in the build pane. When it launches, type (main-) into the prompt and hit enter, and your game will launch.
Then, switch back to your code while the game is still running. Let's modify :on-key-down so the left arrow makes it go right, and vice versa, by swiching the keyword you pass into move:
(= (:keycode screen) (key-code :dpad-right))
(move (first entities) :left)
(= (:keycode screen) (key-code :dpad-left))
(move (first entities) :right)
Now save the file and hit Reload in the build pane. Now try it out! The key bindings have been changed while the game is still running. Now switch the keywords back and reload again.
The next thing to try is reading and modifying state. We'll need to switch the REPL to be in the right namespace, so type in (in-ns 'hello-world.core). Let's peek into the entities list by typing the following into the REPL:
(-> main-screen :entities deref)
That should print out a list with a single map inside of it. Now try moving your image and then run the command again. The :x and :y values should now be updated. You're looking at your game in real-time! Lastly, let's try moving the entity from the REPL:
(-> main-screen :entities (swap! #(list (assoc (first %) :x 200 :y 200))))
Building for Android
- Make sure you have JDK 7 installed (for Windows/OSX, you can get it from Oracle, and for Linux you can get it from apt-get.
- Download the Android SDK. However, don't bother getting the "ADT Bundle", which includes a full IDE, because you'll be using Nightcode. Instead, click "Use an Existing IDE" click the button that appears.
- Extract the file anywhere you want.
- Run the executable called "android" which is located in the "tools" folder of that archive. This executable will display the SDK Manager with several things checked by default. We want to at least support Ice Cream Sandwich, so check the box next to Android 4.0.3 (API 15) and click Install.
- In Nightcode, click on the
androidfolder for your project in the sidebar. You should see a red-colored button called Android SDK. Click that, and find the folder you extracted the SDK to. - Connect your Android device to your computer and make sure USB debugging is enabled.
- Click Run and wait for the app to be built and installed on your device.
Building for iOS
- Get a computer running OS X.
- Make sure you have JDK 7 installed (you can get it from Oracle.
- Install Xcode from the Mac App Store.
- Download and extract the latest RoboVM.
- In Nightcode, click on the 'ios' folder for your project in the sidebar. You should see a red-colored button called RoboVM. Click that, and find the folder you extracted the SDK to.
- Click Run and wait for the app to be built and run in the iOS simulator (the Build button will send it to your device, but you need the certificates set up for that and may need to edit
ios/project.cljto pass the appropriate values to RoboVM).