Java 4K competition

Java4k site

Simple hints

  • It is not about writing nice code!
  • Use only one class, and as few functions as possible. Classes and methods take a lot of bytes.
  • Use a single character for the class file name, variable names will be obfuscated by the compression tool, so it is better to keep them readable.
  • Use primitive objects rather than Objects.
  • Don't worry too much about the initial class size, but instead about the compressed size. Creating a larger class file might generate a smaller jar / pack.gz file.
  • Use Java 1.0 event handlers
  • Use few classes. Each class adds as many bytes as the full package and class name.
  • Use few methods. The usage of the first method-call will add as many bytes as the number of characters in the method name. Once you used a method, the overhead is roughly two bytes.
  • Use local variables, not global fields.
  • Use global, private final static constants to improve readability and help tuning, there is no added cost in doing so.
  • Re-use variables as much as possible. (Usually wait until you have finalised your game as doing this makes your code very hard to read. )

Links

Nice tool for compression here created by Riven great blog!

Getting started at java4k.com

Techniques used in maze4k

4KGamesDesign Wiki a bit old and with some dated information.

Java 4K Tips


Code examples

Start template

import java.applet.Applet;
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;

public class G extends Applet implements Runnable {

	public void start() {
		enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | 
AWTEvent.MOUSE_MOTION_EVENT_MASK);
		new Thread(this).start();
	}

	public void run() {
		setSize(800, 600); // For AppletViewer, remove later.

		// Set up the graphics stuff, double-buffering.
		BufferedImage screen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
		Graphics g = screen.getGraphics();
		Graphics appletGraphics = getGraphics();

		// Some variables to use for the fps.
		int tick = 0, fps = 0, acc = 0;
		long lastTime = System.nanoTime();

		// Game loop.
		while (true) {
			long now = System.nanoTime();
			acc += now - lastTime;
			tick++;
			if (acc >= 1000000000L) {
				acc -= 1000000000L;
				fps = tick;
				tick = 0;
			}

			// Update
			// TODO add some update logic here.

			lastTime = now;

			// Render
			g.setColor(Color.black);
			g.fillRect(0, 0, 800, 600);
			g.setColor(Color.white);
			g.drawString("FPS " + String.valueOf(fps), 20, 30);

			// Draw the entire results on the screen.
			appletGraphics.drawImage(screen, 0, 0, null);

			do {
				Thread.yield();
			} while (System.nanoTime() - lastTime < 0);

			if (!isActive()) {
				return;
			}
		}
	}

	public void processEvent(AWTEvent e) {
		// Process the key event.
	}
}
import java.applet.Applet;
import java.awt.*;
import java.awt.image.*;

/**
 * Testbed for the basics.
 */
public class Skeleton extends Applet implements Runnable
{
	// Constants.
	// Must be at least 1026 if using Java 1.0 event model or someone accidentally
	// hitting Ins will get AIOOBE. Pick a number used elsewhere and reuse it to
	// save a constant pool entry.
	private static final int NUM_KEYS = 1026; 

	private static final int WIDTH = 640;
	private static final int HEIGHT = 480;

	// Shared state. In the spirit of lock-free synchronisation, this is written
	// only by the event thread, and it advances values rather than setting /
	// resetting. This ensures that quick key presses aren't lost.
	private int[] keyEventCount = new int[NUM_KEYS];

	@Override
	public void start()
	{
		new Thread(this).start();
	}

	public void run()
	{
		// Set up event handling. Note: keyDown[key] and keyPressed[key] are 0
		// for false; any other value should be interpreted as true.
		int[] prevKeyEventCounts = new int[NUM_KEYS];
		int[] keyDown = new int[NUM_KEYS];
		int[] keyPressed = new int[NUM_KEYS];

		// Game state.
		int x = 320;
		int y = 320;

		// Set up backbuffer for rendering.
		BufferedImage buf = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
		// Uncomment if you want to set pixels directly.
		//int[] ibuf = ((DataBufferInt)buf.getRaster().getDataBuffer()).getData();

		while (!isActive()) /* Busy-wait */;
		while (isActive())
		{
			try
			{
				long now = System.currentTimeMillis();

				// Events.
				for (int i = 0; i < NUM_KEYS; i++)
				{
					int eventCount = keyEventCount[i];
					// An odd number of events means that it's down. Simple!
					// NB If you're worried about key repeat you might want
					// instead to say
					//     keyDown[i] = (delta >> 1) + (eventCount & 1);
					keyDown[i] = eventCount & 1;
					// The key was pressed since the last loop if the number of events
					// since then is at least two, by the pigeonhole principle, or if
					// the number of events since then is positive and the key is down.
					// Since we only need to catch the second case when delta == 1,
					// we can simplify the key-is-down test a bit.
					int delta = eventCount - prevKeyEventCounts[i];
					keyPressed[i] = (delta >> 1) + (delta & eventCount);
					prevKeyEventCounts[i] = eventCount;
				}

				// Logic.
				if (keyDown[Event.LEFT] != 0) x--;
				if (keyDown[Event.RIGHT] != 0) x++;
				if (keyDown[Event.UP] != 0) y--;
				if (keyDown[Event.DOWN] != 0) y++;

				// Render to backbuffer.
				Graphics g = buf.getGraphics();
				g.setColor(new Color(0x000000));
				g.fillRect(0, 0, WIDTH, HEIGHT);
				g.setColor(new Color(0xffffff));
				g.drawString("Score", x, y);

				// Render backbuffer to front buffer and tidy.
				Graphics gapp = getGraphics();
				if (gapp != null)
				{
					gapp.drawImage(buf, 0, 0, this);
					gapp.dispose();
				}
				g.dispose();

				// Sleep. There are various ways of doing this, but IME you need to
				// sleep rather than yield under Linux.
				long t = System.currentTimeMillis();
				long sleep = 20 - (t - now);
				now = t;
				if (sleep > 0)
				{
					Thread.sleep(sleep);
				}
			}
			catch (Throwable ie)
			{
					
			}
		}
	}

	@Override
	public boolean handleEvent(Event evt)
	{
		// Non-key events have 0 for their key field.
		keyEventCount[evt.key]++;
		return false;
	}
}

Sound

Play effects and music: code example from angryoctopus

source from javagaming.org:

/*
 * S.java
 *
 * Created on 14 April 2009, 14:34
 */

/**
 *
 * @author  Alan Waddington
 */
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Clip;

import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;

public final class S extends JFrame {
	private final static int SCREENHEIGHT = 480; // Screen height
	private final static int SCREENWIDTH = 640; // Screen width

	private final static float RATE = 16000f;// Music sample rate
	private final static float AMPLITUDE = 4000f; // Music amplitude

	private byte musicLoop[]; // Array of 16 bit music sound samples
	private Clip music; // Clip to play the above music track

	int maxNote = 261;
	int length = 512;
	int tempo = 200;
	String notes = "2gK8 I9k&+.y{^10[.p0l\"V^#pvoM$+#N8 I9k&+.y{^1%%bo0h2#^#Y4L8 \\4L8 \\4L8 Y4L8 f1M&$}xV^#g\"V^#j\"V^#QTd^#57@^#g\"V^#*55N$z=Y_1O(// aVU^#Nd1^#1Gl]#Pau7*g\"V^#l\"V^#Qt]]#DTd^#g\"V^#l\"V^#&oj%$M.A^#NTd^#57@^#Nd1^#1Gl]#Jd1^#1Gl]#,Gl]#77@^#57@^#3i481}xV^#l\"V^#Qt]]#KTd^#g\"V^#l\"V^#27@^#h2#^#c2#^#h2#^#Jt]]#6tx%$o+M8 V4Y_ o+M8 V4Y_ d[2^#1Gl]#e2#^#Jt]]#?i3t$Fw%^#8-0&$`k^]#,W9]#k)H]#ZDx7 <T}6*r_/&$v'}6*r_/&$svP7*h2#^#qxz]#1Gl]#@DQ7*h2#^#h2#^#c2#^#Jt]]#p)H]#F%+]#+/U%$P}T7 :'T7 4'a^ P}T7 :'T7 :'T7 :'T7 :'T7 :'T7 F%+]#A%+]#{W*]#uVT%$lKF7 4'a^ f+g'!)#N8 Jt]]#,W9]#MyDL$V2<6*{W*]#PR7%$*e;6*{W*]#{W*]#{W*]#{W*]#{W*]#_*9]#n9t\\#:'T7 VTE7 4'a^ lKF7 4'a^ f+g'!)#N8 Jt]]#,W9]#%U   ";

	/** Creates a new instance of S */
	public S() {
		// Create array of music frequencies
		double[] freq = new double[100];
		double f = 16.351562; // Frequency of C0 (C4 is middle C)
		for (int i = 0; i < 7 * 12; i++) {
			freq[i] = f;
			f *= 1.0594630944; // Evenly tempered scale (12th root 2)
		}

		musicLoop = new byte[(int) (length * 30f / tempo * RATE)];

		// Read each note and render it into the loop
		int semiQuaver = 0; // Start time for next note
		int byte0, byte1, twoNotes = 0;
		for (int i = 0; i < maxNote; i++) {
			if ((i & 1) == 0) {
				// Decode 5 chars to 1 int
				twoNotes = 0;
				for (int j = 4; j >= 0; j--)
					twoNotes = twoNotes * 95 + (int) notes.charAt(5 * (i >> 1) + j) - 0x20;
			}
			byte0 = twoNotes & 0xff; // Frequency
			byte1 = twoNotes >> 8 & 0xff; // Delay & Duration
			twoNotes = twoNotes >> 16;
			double frequency = freq[byte0 & 0x7f];
			if (byte0 > 127) // If bit7 is set, there is a delay before note
				semiQuaver += (byte1 >> 4 & 0x0f) + 1; // Note start time
			int duration = (byte1 & 0x0f) + 1; // Note duration
			// Calculate start and stop times in terms of samples
			int start = (int) ((semiQuaver) * 15f / tempo * RATE);
			int stop = (int) ((semiQuaver + duration) * 15f / tempo * RATE);

			// Calculate each sample and render it into the music loop
			for (int j = start; j < stop; j++) {
				double time = (j - start) / RATE; // Time (seconds)
				// Triangle fundamental + sinusoidal 4th and 6th harmonics
				double sin4 = (Math.sin(Math.PI * frequency * time * 8.0));
				double sin6 = (Math.sin(Math.PI * frequency * time * 12.0));
				double triangle = 2.0 * ((frequency * time) % 1.0);
				if (triangle > 1.0)
					triangle = 2.0 - triangle;
				triangle = 2.0 * triangle - 1.0;
				double sample = 0.5 * triangle + 0.2 * sin4 + 0.2 * sin6;
				// Amplitude modulate the lot at about 2Hz
				sample *= 0.6 + 0.4 * (time % 0.5) / 0.5;
				// Apply attack and decay to avoid clicks
				if (j - start < 1000)
					sample *= (j - start) / 1000f;
				if (stop - j < 1000)
					sample *= (stop - j) / 1000f;
				// Add the 16 bit sample to loop
				int data = (musicLoop[2 * j] << 8) + (musicLoop[2 * j + 1] & 0xff);
				data += AMPLITUDE * sample;
				musicLoop[2 * j + 1] = (byte) (data & 0xff);
				musicLoop[2 * j] = (byte) ((data >> 8) & 0xff);
			}
		}
		try {
			// Initialise Sound System
			AudioFormat audioFormat = new AudioFormat(RATE, 16, 1, true, true);
			DataLine.Info info = new DataLine.Info(Clip.class, audioFormat);
			music = (Clip) AudioSystem.getLine(info);
			music.open(audioFormat, musicLoop, 0, musicLoop.length);
			music.loop(Clip.LOOP_CONTINUOUSLY);
		} catch (Exception e) {
			e.printStackTrace(); // Display error, but keep going
		}

		// Screen
		BufferedImage screen = new BufferedImage(SCREENWIDTH, SCREENHEIGHT, BufferedImage.TYPE_INT_RGB);
		int[] screenData = ((DataBufferInt) screen.getRaster().getDataBuffer()).getData();
		Graphics g;
		setSize(SCREENWIDTH, SCREENHEIGHT);
		show();

		while (isVisible()) {
			int position = music.getFramePosition() % musicLoop.length;
			for (int x = 0; x < SCREENWIDTH * 20; x++) {
				int xx = x / 20;
				int p = (2 * position + 2 * x) % musicLoop.length;
				int note = 256 * musicLoop[p] + (musicLoop[p + 1] & 0xff);

				for (int y = 0; y < SCREENHEIGHT; y++) {
					screenData[xx + y * SCREENWIDTH] = 0;
				}
				int yy = SCREENHEIGHT / 2 + note / 32;
				if (yy < 0)
					yy = 0;
				if (yy >= SCREENHEIGHT)
					yy = SCREENHEIGHT - 1;
				screenData[xx + yy * SCREENWIDTH] = 0xffffff;
			}
			g = getContentPane().getGraphics();
			g.drawImage(screen, 0, 0, null);
			g.dispose();
		}
	}

	/**
	 * @param args
	 *            the command line arguments
	 */
	public static void main(String[] args) {
		new S();
		System.exit(0);
	}

}