Writing a reasonable Java 4K game is no trivial task (not to me, at least). You have to make very careful decisions about how to invest the tiny size asset you have : will you invest in graphics? In gameplay? In AI? In sound?
Before going into details, remember two crucial points: First - your aim is to produce the smallest archive, not the smallest class file. There is a huge difference between both approaches. In the first one, your aim is to use many similar sequences of instructions, even code redundantly on-purpose, all aimed at helping the compressor produce better compression ratios. Second - you are not constrained in how you use memory - you may have arrays with holes, just for the sake of redundancy, you may allocate big structures and fill them with calculated values, etc.
Here's a list of short tips I came upon after during my Java 4K involvement. I hope you find them useful.
1.Ignore class size and watch the compressed, optimized final size instead, when comparing alternative approaches.
2. Use an optimizer like ProGuard, JoGa or even better, use a tool like moogie's excellent 4KJO optimizer, which runs several optimizers and compressors in different sequences to determine the best optimization/compression sequence. 4KJO is quite processor-intensive and each run takes its time, but it's well worth the effort.
3. Favour different draws instead of string concatenations.
NO : g.drawString("Score :"+String.valueOf(score),400,60);
YES : g.drawString("Score :",400,60);
4 . Avoid making references to more classes than strictly necessary. The name of each referenced class is inserted literally in the class file. So don't declare Image variables if you have already referenced BufferedImage, don't declare AWTEvent if you alread have KeyEvent, etc...
NO: int y = Math.max(a,b);
if (a>b) y =a; else y=b;
5. Don't write frameworks! Stick to what you need.
6. Write all the code in a single method. In this way, you also minimize the need for global variables. If you have troubles organizing your code in a single method, then write a multi-method class and inline the methods manually when you are mostly done. Yes it's ugly. But it's small.
7.Use global, final static constants to make code readable. They don't impact the final size.
8. Implement the basic game mechanics first, using rectangles instead of graphics. After you have your basic game working, you can spend the remaining bytes available in graphics/sound and then in extra mechanics. It's of no use if you start displaying wonderful graphics but end up with no space for the game itself. A game must be FUN TO PLAY FIRST, and then the rest
9.Keep a log with the size of the compressed, obfuscated jar after each feature. In this way, you can always backtrack removing a specific feature if you need the bytes later. (See my own logs in the source code of my own 4K games)
10. Don't use sprites unless you are using many many colors. Draw the graphics using graphics primitives. Order your graphics operations properly in order to avoid long sequences of digits. Remember, you are not coding for speed, but for size. And in a 32x32 sprite, there won't be much difference if you use 10 colors instead of 40. Unless you do something crazy, speed is almost a given in today's processors for these kinds of games
11. Use whatever symmetries are available in your graphics. Use g.rotate(), g.translate() and g.scale() freely
12. By the time you reach 3900 bytes of compressed jar, your game should be release-ready. If it is not, do whatever is necessary to make it releasable before adding any new graphics or features.
13. Don't try to beat the compressor. If you ever find yourself assigning meanings to bits, pause and check the direct approach.
14. Don't write interpreters. This is a specific case of rule 4.
15. Reference all the classes you'll need at the beginning of your project so that there are no surprises afterwards with the size of the constant pool.
16. Using a single array for storing all kinds of data (position, speed, status) of all actors on a scene (enemies, shots, etc.) gives only a very marginal improvement in the final compressed jar (4072 vs 4012 bytes in the case of Dookie. Dookie uses 6 arrays of data. Replacing them with a single array and optimizing access yielded an improvement of only in my experience, 40-50 bytes at most). Use it only as a final remedy - it is troublesome and error-prone. Create a separate array for each piece of data.
17. Always put a fps counter on the title. Use it as a guide, and as a safe buffer for gaining some bytes when you remove it.
18. Reuse variable declarations to increase code redundancy. It takes more space to have
for (int i = 0; ...) ten timesthan
int i; 1 time for (i = 0; ...) 10 times
19. Remove any statement-local variables (variables declared within an if, loop, else, etc..)
and replace them with method declarations that are reused many times. Remember microprocessors? An even more radical approach would be to forget using local variables.
Declare a bunch of method-scope integer variables ("ax,bx,cx...") and reuse them
constantly. This increases the redundancy of code
20 . Increase duplicated information and decrease number variations! Imagine you have
g.setColor(new Color(240,250,100)); ... g.drawString("Score",120,255);Well, probably no one (except the compressor) will note the difference if you change the above to:
g.setColor(new Color(250,250,100)); ... g.drawString("Score",100,250);
and the second one will compress better.
Another example. At one point I had
g.setColor(Color.getHSBColor(i* 60/360f, 1, 1));
I assumed that the compiler would be clever enough to do the literal math itself. Well
probably it didn't, because after replacing the above with
g.setColor(Color.getHSBColor(i/6f, 1, 1));
I got 8 bytes less in the final jar.
22.Sometimes, just reordering the instructions will cause a drop in the compressed result. In one case, I had
int score = 0; int level = 0; int animals = 0; int weight = 0; int lives = 5;
and reordering to
int score = 0; int lives = 5; int level = 0; int animals = 0; int weight = 0;
caused a drop of 10 bytes in the final jar! Go figure. These kinds of 'optimizations',
however, should be your very last option (see 23)
23. Simplify calculations if the end result is reasonable close. Nobody is going to notice if that bullet is off the perfect sine trajectory by 1 pixel.
24. If you have to store a set of numbers, try to think *hard* if there's a way of calculating them
on the fly. It's almost a sure bet that an array with that describes how enemy health, intelligence, whatever changes with the level can be replaced with a calculation, and by doing this you not only reduce size, but you gain infinite levels.
25. Bear in mind that by doing the most convoluted optimizations (like instruction reordering
and the like), your program becomes more and more 'metastable', which means that a very
small change (even changing a single number) can cause a big increase in the compressed .jar. The convoluted optimizations should be left for the moment where you are done with the code, have no intention to add or change anything, and just want to scratch a few tens of bytes to fit the thing into the limit
26. Don't use external resources. All your resources should be embedded in the class file. Every external file (be it a single image, or a MANIFEST.MF) means an additional entry in the JAR file, and this is a huge (comparatively) expenditure of bytes. If you absolutely must use external resources, rename each file to a single letter, without extension.
27. And of course, your own class should have a single-letter name
28. Create a template class with the bare-bones minimum that each game must have (window initialization, event handling, etc.) and use it always as a tested starting point.
29. Do not use listeners for handling input events, override processXXXXEvent instead (procesKeyEvent, processAWTEvent...)