Skip to content

Commit

Permalink
Add text
Browse files Browse the repository at this point in the history
  • Loading branch information
triplefox committed Dec 14, 2015
1 parent e21197c commit 76e54d8
Show file tree
Hide file tree
Showing 22 changed files with 8,211 additions and 0 deletions.
4,649 changes: 4,649 additions & 0 deletions docbook/book.xml

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions markdown/__roadmap.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
roadmap

* put the project on github
* put the docs on github
* link the assets to the github project
* Fix up all the FIXME and TODO lines
* get it reviewed
* publish a nice-looking version
* add it to my resume
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
We will make a clone of the 70's Atari game "Canyon Bomber". In this game, a plane flies over a canyon in side view and drops bombs, trying to clear away all the rocks on the board without missing. It's one of the earliest "one-button games" and presents a well-rounded exercise for Kha's toolset.

In this passage we'll work on moving a rectangle across the screen over time, introducing timing and simple animation. The rectangle will later become the "bomber plane" in our game.

## Timing in Kha

Kha contains a sophisticated timing and scheduling system, but also lets you dig into raw timing data.

Scheduler.addTimeTask(game.simulate, 0, 1 / 60);

This uses the Scheduler API to call game.simulate 60 times a second(every ~16.6 ms), indefinitely.

### Period vs. Duration

Period is the total length of a scheduled task. Duration is the frequency at which the task is repeated.

For example, if you wish to call an event 30 times a second, but for only 2 seconds, you would add a task with a duration of 1/30 and a period of 2.

### realTime() and time()

To get the current time since start, you can use:

Scheduler.time();

and

Scheduler.realTime();

Scheduler will automatically apply smoothing, limiting, and atomization to timing data so that the average-case experience is as jitter and pause-free as possible.

* Scheduler.time() is the time that has passed after these adjustments.
* Scheduler.realTime() is the system global time, or "wall clock" time. Use this one for performance data, or if you want to do your own timing from scratch.

### Why atomic timing is useful

It is tempting to plug in a delta time into your physics equations, tweens, etc. and allow the time to float freely. Sometimes this is even a correct approach.

However, there are two reasons not to do this. One is that it makes physics simulations inconsistent - often wildly so. Jump heights will be unpredictable, people will go flying at strange velocities, etc. The other is that it hurts reproducability otherwise, for example if you are recording a demo for playback.

Therefore, the most flexible approach is to chop up delta times into regular "atoms" of time, and play through them one at a time. This is what Kha does inside its Scheduler API. It is even designed to support complex use-cases where timers get called in a certain order, or are paused or cancelled while active.

## Drawing rectangles

We use Graphics2.fillRect() to draw some rectangles. In doing so, we also make use of the "color" attribute on Graphics2. We already used some color to do our full-screen strobe, but here we can see more clearly what is going on.

Graphics2 uses a "finite state" model of rendering, where we describe a configuration for drawing, and then issue a command that updates the state of the framebuffer we are drawing to. So when we change color, that changes the color of the drawing commands we issue after that change.

## Scaling

Oh, how inconvenient! We aren't always going to get the resolution we request returned in System.pixelWidth and System.pixelHeight. After all, the system can't always give you what you ask for.

That also means that if we want to force a certain pixel resolution - and this is the type of game where that happens - we're going to have to think about scaling.

There are two ways we can approach this.

1. The more "pixel-perfect" way would be to have a "*back buffer*". Instance an Image at our ideal resolution, draw the gameplay on that, then draw that, scaled, to the display buffer. That guarantees that everything stays on the same pixel grid, no matter what we do.
2. But we'll go the other route of adding a *scaling transform* using a FastMatrix3 before our drawing calls. This will give us the correct proportions, and it avoids an additional trip through the GPU, so it may be somewhat faster, but it means that objects may look "wrong" if they are scaled, rotated, or placed on subpixel coordinates.

It's very easy to swap between these two methods if you prefer one or the other. Variations like only scaling to integer factors of the original, cropping the image, etc. are also possible.

## The plane's data structure and algorithms

I decided to make the plane a Haxe Typedef with an x and y position, x and y velocity, and a width and height. Although we don't have to define a width and height as a *variable* on the plane - it's never going to change during gameplay - it's convenient for us, so I splurged.

The screen space and the physics space in Canyon Bomber are 1:1, and the camera is locked in one position, so no coordinate conversions have to take place during rendering - I just enter the position and size directly. If I wanted to scroll the screen or zoom in and out, this part would become more complex.

Then I wrote a routine to move it across the screen each update, and make it wrap around as it runs off the edge. This routine will get more complex later(and so will the plane's data).

---

### Empty.hx
```haxe
package;
import kha.Framebuffer;
import kha.Color;
import kha.input.Keyboard;
import kha.input.Mouse;
import kha.Key;
import kha.math.FastMatrix3;
import kha.System;
class Empty {
public function new() {
if (Keyboard.get() != null) Keyboard.get().notify(onDown,onUp);
if (Mouse.get() != null) Mouse.get().notify(onDownMouse,onUpMouse, null, null);
}
public static inline var WIDTH = 320;
public static inline var HEIGHT = 240;
public var fire = false;
var plane = { x:0., y:0., w:8., h:8., vx:1., vy:0. };
public function onDown(k : Key, s : String) {
trace(s + " down");
fire = true;
}
public function onUp(k : Key, s : String) {
trace(s+" up");
fire = false;
}
public function onDownMouse(button : Int, x : Int, y : Int) {
trace('$button down');
fire = true;
}
public function onUpMouse(button : Int, x : Int, y : Int) {
trace('$button up');
fire = false;
}
public function render(framebuffer: Framebuffer): Void {
// color settings
var col_bg = Color.Black;
if (fire)
col_bg = Color.Red;
var col_plane = Color.White;
var transform = FastMatrix3.scale(
System.pixelWidth / WIDTH,
System.pixelHeight / HEIGHT);
{ // graphics2 calls
var g = framebuffer.g2;
g.begin();
g.clear(col_bg);
g.pushTransformation(transform);
g.color = col_plane;
g.fillRect(plane.x, plane.y, plane.w, plane.h);
g.popTransformation();
g.end();
}
}
public function update(): Void {
{ // advance plane movement
plane.x += plane.vx;
plane.y += plane.vy;
// wrap around
if (plane.x > WIDTH)
plane.x = -plane.w + 1;
else if (plane.x < -plane.w)
plane.x = WIDTH + 1;
}
}
}
```
53 changes: 53 additions & 0 deletions markdown/canyon_bomber/api_graphics_classes.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Kha API 2: Display

Now we will render a very simple "strobe the screen with different colors". **Caution: this may be seizure-inducing.** (lower the display size if you're worried)

### Empty.hx
```haxe
package;
import kha.Framebuffer;
import kha.Color;
class Empty {
public function new() {
}
public function render(framebuffer: Framebuffer): Void {
var nextcolor = [Color.White, Color.Red, Color.Blue, Color.Green, Color.Black][Std.int(Math.random() * 5)];
var g = framebuffer.g1;
g.begin();
for (y in 0...framebuffer.height) {
for (x in 0...framebuffer.width) {
g.setPixel(x,y,nextcolor);
}
}
g.end();
}
public function update(): Void {
}
}
```

## What is "backbuffer.g1"?

Kha contains different numbered "levels" of its graphics API. Graphics1 is the simplest possible system: it can plot pixels only.

Here is a replacement of that block using Graphics2:

```haxe
var g = framebuffer.g2;
g.begin();
g.clear(nextcolor);
g.end();
```

Graphics2 adds more "standard" functionality, including clearing the screen, drawing shapes and blitting images. It's for general-case 2D applications, and its routines can be counted on to be faster than plotting individual pixels. We'll expand on Graphics2 in later chapters.

There is also a Graphics4, which exposes the full shading pipeline. In theory Graphics3 represents fixed-function GPU pipelines, but it does not exist at this time. We will not venture into Graphics4 in this guide, and focus instead on fleshing out a game in Graphics2.

## Why is there a begin() and end()?

Kha batches the graphics operations internally and processes the actual drawing commands when it reaches end(). This lets you code your own rendering logic more freely, while letting Kha optimize the commands to whatever works best in the target environment.
79 changes: 79 additions & 0 deletions markdown/canyon_bomber/api_keyboard_mouse_input.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
Next, let's respond to the keyboard and mouse. Kha provides raw input abstractions - you will still have to provide the high-level stuff like user customization.

For our game, we're only going to have one button. We'll take this an extra step by also mapping the mouse buttons. The example code now toggles the screen between white and black when the button is pressed.

### Empty.hx
```haxe
package;
import kha.Framebuffer;
import kha.Color;
import kha.input.Keyboard;
import kha.input.Mouse;
import kha.Key;
class Empty {
public function new() {
if (Keyboard.get() != null) Keyboard.get().notify(onDown,onUp);
if (Mouse.get() != null) Mouse.get().notify(onDownMouse,onUpMouse, null, null);
}
public var fire = false;
public function onDown(k : Key, s : String) {
trace(s + " down");
fire = true;
}
public function onUp(k : Key, s : String) {
trace(s+" up");
fire = false;
}
public function onDownMouse(button : Int, x : Int, y : Int) {
trace('$button down');
fire = true;
}
public function onUpMouse(button : Int, x : Int, y : Int) {
trace('$button up');
fire = false;
}
public function render(framebuffer: Framebuffer): Void {
var nextcolor = Color.White;
if (fire)
nextcolor = Color.Black;
var g = framebuffer.g2;
g.begin();
g.clear(nextcolor);
g.end();
}
public function update(): Void {
}
}
```

# kha.input.Keyboard

This is an abstraction for keyboard input. It has a get() method that optionally takes a number. This allows more than one keyboard to be detected and used by Kha; a similar pattern applies for all input devices.

Once you have a Keyboard, register callbacks using notify(). The Keyboard callback uses these arguments:

key : Key, string : String

where "key" is the Key enum and "string" is the character value of the key.

# kha.Key

This is an Enum for the keypress type. Some frameworks monitor "held" keys like Shift, Ctrl or Alt and pass them in as flags on the keyboard event, but Kha takes the approach of sending a new event for each one. It also includes some non-character keys like the arrow keys in this enum.

For everything else, Kha passes in string data to the Keyboard callback.

# kha.input.Mouse

As with Keyboard, Mouse.get() returns a device instance and notify() registers callbacks. The Mouse callback uses these arguments:

button : Int, x : Int, y : Int

where "button" is the button pressed and "x" and "y" are the screen coordinates.

Mouse additionally has methods for locking the cursor focus and hiding its display.
62 changes: 62 additions & 0 deletions markdown/canyon_bomber/api_main_class.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Kha API 1: Main class, System

The Empty project already has Kha configure some things about the display and update timing, so let's review. For laziness and simplicity reasons, we're going to stick with the "Empty" naming convention throughout this guide, although you may want to pick a cuter name for your project's title and main class.

### Main.hx
```haxe
package;
import kha.Scheduler;
import kha.System;
class Main {
public static function main() {
System.init("Empty", 640, 480, initialized);
}
private static function initialized(): Void {
var game = new Empty();
System.notifyOnRender(game.render);
Scheduler.addTimeTask(game.update, 0, 1 / 60);
}
}
```
### Empty.hx
```haxe
package;
import kha.Framebuffer;
class Empty {
public function new() {
}
public function render(framebuffer: Framebuffer): Void {
}
public function update(): Void {
}
}
```

Running this should still give you a blank screen. We have three Kha classes to look at:

## System

This class is the "global state" of your Kha app: It holds the render callback, it says how big the screen is and how fast it refreshes, etc. We use it here to initialize the screen, and then to set up the rendering callback. System also contains:

* callbacks for various OS/windowing-level events, like being minimized or shutdown
* a global "time" value (since startup, measured in seconds)
* vsync control
* systemID - what target you are on, e.g. "HTML5".

## Scheduler

This class governs all the interesting timing information in Kha. We'll discuss it in depth later. For now, just know that it lets you set up recurring tasks like updates to your gameplay, independently of the render callback. Although System has a "time", Scheduler has a "framerate-corrected" time, so consider using it first if the time is used for gameplay.

## Framebuffer

This is a class that represents the display we're drawing to from the rendering callback. This will get more attention shortly, as our next API chapter is about simple drawing.

Loading

0 comments on commit 76e54d8

Please sign in to comment.