Thanks Spencer! A few of those are finished, though most are WIPs (though they've all been finished since around last November)
Today I made a huge design/programming post!!! For the first time...
Designing ETO's World Map: Part 1 (Of ... ?)This week, we decided to finally flesh out the design details of ETO's world map.
Here's what we knew going into this:- It's a hub to the main areas of the game
- It is like a standard JRPG world map
- You traverse it like the early Final Fantasies/Dragon Warriors
- You only walk on it
- A few details about the placements of the main areas
I'd already implemented a system for a player to walk around the world map with an alternate sprite.
Here's what we needed to decide:- The size of the world map
- The speed of walking in the world map (largely dictated by the size and content)
- Where to position the main areas
- How to design the paths to the main areas
- Adding anything else to the world map
- Visual perspective of the world map
For the most part, we figured that out this week.
So early in development, I made a few world maps for what I thought we'd need. This changed, of course. Eventually I made some drawings, rough drawings - with the relative positions of the areas. So that's more or less, what we started with, visually.
This is what I've been using in place of the world map during development. It's just a small hub to walk between the areas. Each icon is an area, you stand over it, and interact to enter.
Visual perspective of the world mapWe started with this task first. The whole reason we were even working on the world map was in preparation for the IGF Deadline (which we're not actually submitting to - just as a motivator).
Because a normal, flat perspective would be awkward, Joni wanted to do a perspective effect.
Joni asked if it was possible to do a screen-warping effect similar to terranigma:
https://www.youtube.com/watch?v=Nsb2eFt7BCs&feature=youtu.be&t=3h3m57sMy initial thought was "well, maybe". I think - though I'm not sure - that Terranigma was using the SNES's Mode 7. That's my guess, as my final solution was more or less re-implementing that in software, without knowing it (I just looked up that Mode 7 was a way to scale and transform every scanline on the screen).
Attempt 1: Use multiple cameras showing different parts of the map.The engine, HaxeFlixel, lets you add cameras to the game to show different parts of the screen. I thought I could use these to show distorted chunks of the screen to 'look ahead' - as I sort of expected, the math behind this was kind of messy, it didn't look good at all, and the performance was probably really bad, not to mention it would cause issues with the way the pause menu works. A lot of uninteresting things. I scrapped the idea.
Attempt 2: Display the background (bg) in 8px strips, and scale the strips along the x axisSo after realizing the brokenness of the previous method, Joni sent me a racing game tutorial. The idea was to break a flat image into chunks, and scale them along the x-axis. I was worried about performance ahead of time like usual
(don't do this) and so I figured out a solution pretty easily by treating the world map as a single spritesheet - the world map is w by h pixels, for each row on the screen, make a sprite, have it load the world map as a sprite sheet where each frame is w by 8 pixels, and give it h/8 many animations.
Change its animation based on its screen-row index, and the y-position of the camera.
This looked okay, but there was some noticeable banding around where the chunks separated.
For this method I needed (h/ch) + 1 chunks (ch = chunk height). Because there were only (h/8) animations, the chunks actually had to move with the camera so many pixels before they changed animations. Chunk 'i', would start at (i/8) on the screen, playing a certain animation. It doesn't move with the camera (think fixed GUI text). So as the camera displaying the original world map moved down, chunk i will have to move up. After the camera moves up 8 pixels, the chunk must play a new animation.
Because of this, there would be an non-chunked gap int he bottom as I moved. So I needed 1 more than (h/ch) chunks.
Scale itTo make a perspective effect, the further up a strip was, the more shrunk it was in the x direction. The further down it was, the more wide it was.
This was fine, but it looked kind of funny, so I tried 1 px chunks.
Attempt 3: Same as above, smaller strips.Eventually I realized that 1px chunks were ok, because I only needed 256 to fill the entire screen. Also, this way the chunks would never need to move, because there would be one animation from the spritesheet for each row of pixels in the spritesheet.
It turned out the performance on this was fine, and it actually looked pretty okay.
So here's how the method looks, first pass.
this is a test world mapThis is me messing around, of course, when I was only testing a section at the top of the screen. But the general idea was there.
Here's a better attempt at using it:
This is nice, but Joni noticed it looked sort of funny, because if this was a traditional perspective drawing, the top rows would also be shrunk in the y direction. But, we can't shrink a 1 pixel sprite in the y-direction! Oh no!
Method 4: Scaling in the y, pass 1.Scaling something down - it's basically picking pixels that make the smaller image still sort of look like it. Sometimes, when the target size doesn't have enough pixels, you can interpolate things. Most scaling software does this. I can't really afford that sort of thing at 60 FPS in software, especially with the current method.
So we wanted scale stuff down gradually nearer to the top, and scale stuff up nearer to the bottom, to give a perspective effect.
Since the player is centered in the world map, as long as that part of the map isn't too distorted from the original image, the collisions in the final map will still feel fine.
Initially what I did was - the further a chunk was from the center of the screen, the "further away" from its correct row it would show.
As this image shows - the map seems to move much slower near the top. This is because I said - if a row is within 16 pixels of the top of the screen (in screen-space), then make it display 4*(16-i) pixels further up than it should.
This is a hint of what we were going for, but obviously has that awkward boundary, and the y-scaling isn't throughout the entire screen
Method 5: Screen-wide scaling, naive displacement-functionSo, I would need to offset stuff amongst the whole screen, and I would want to do this algorithimically so that it's easy to tweak.
The first thing I tried was the same as above - but every pixel affected.
The general aim was: within 16 pixels of the center, display the 'correct' animation. Between 16-32, display "1 pixel away" from the row below it. And so on. This was pretty easy to do. I used sequences for this.
The sequence, S, more or less, was like 0,1,1,1,1,...2,2,2,2,...
Think of that as the "distance from lower neighbor" for pixel 128, 127, 126, ... , 0. (S_0, or 0, is the 0th element of the sequence S, or in this case, is pixel 128's 'distance from lower neighbor' is an edge case - it's treated as 0 for these purposes.)
So to get the actual offset from the center (128), you sum all values before a given value, including that value, and that's the offset you use.
E.g.:
offset_i = sum(128,i) v_i
where v_i = element (128-i) of sequence S.
Here's a funny bug from working on this.
For the careful reader: you may have noticed: this totally won't work for the bottom half of the screen - we want it *closer* to us, not further.
But... how do we do this? Well... in theory, the further to the bottom you get, the more likely a given row
is a duplicate of the row above it.
Thus our sequence, from row 128 to 255, would look something like
0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0
But...how do we generate such a sequence?
Method 6: Generating such a sequence via ditheringTurns out that such a sequence was also necessary for fixing a new problem with the top half of the screen: the noticeable borders from where distances between pixels increased. i.e., in the sequence - where 1 became 2, and 2 became 3, etc - you could see some meh seams.
Enter dithering!!!
Dithering is more or less interpolating between two values. We used the word 'dithering' for some reason. In pixel art, it's giving the illusion of shading by (Google it), etc.
So I made this complicated method that used floor() and ceil(), but then joni made a cool visual chart to show that we could use rounding to get the desired effect.
The visual intuition was - if you take the background image, and start drawing horizontal lines from the top, but draw them successively clsoer -if you draw 256 of those lines, and round each line to the cloest row, then if you play those animations, you get the desired effect.
When it was implemented in code, the idea was:
- Generate a array of 255 values, showing each row's 'distance' from its upper neighbor. This sequence, S2 looks something like 0.58, 0.6, 0.7, .....
- Use the same summation method to get each row's cumulative distance from the bottom
- Because the center of the screen should always display (Camera_y + 128), subtract off S_128 from everything so that S_128 = 0 and everything else represents how far that row is from row 128
- round all the values in the sequence to the nearest integer
Now, we have a sequence, S3, like:
-10,-8,-7,-6,...,0,...,67,68,69,70,70
Meaning row i should play animation #(camera_y + 128 + S3_i + S3_128).
If you'll notice - the higher a row on the screen, the lower to the left in that sequence it takes. So the further away you are, the more likely you are to 'skip' a row, giving the illusion of being further away.
Likewise, the lower ar ow on a screen, the more likely to play a duplicate.
The equation we ended up using was just linear. so a c_lo + (c_hi - c_lo) * (i/256) = S2_i.
If you'll notice - the distances between the numbers in S3 are precisely what we wanted for the "distances between pixels' function. The example I gave is 2, 1, 1, ..., 1, 1, 1, 0, 0.
And this looked good! Here's a bug I came across while doing it
I added some controls to let Joni tweak c_lo and c_hi and we settled on good values.
Here's the perspective we settled on: as you can see, Joni added boxes and lines to the image, to see how they distort, to achieve the desired perspective. The different pictures are testing different mist overlays with different blend modes.
Hooray! We did it... right?
Addendum: oh wait some stuff looks badOkay, so small , rounded pictures on the map, while the camera moved up or down, had some sort of 'seam' you could tell, from where there was a "hole" in the distance-between-neighbor sequence, something like 1, 1, 1, 2, 1, 1 - the two indicates a skipped pixel. So I needed to add some random jitter to the screen.
Basically how I achieved this was by : if the camera y position modulo 3 is 0, if a row index is even, and the player is moving up or down, have that row play a pixel below what I decided it should play.
It works good enough.
wait it's hella slow and leaks memoryThis had to do with using 256 sprites or something. I ended up refactoring some code to use one sprite and just have the sprite redraw itself to the camera with a different animation, y scale, and y position. It's faster and uses much less memory and now doesn't leak anymore, or whatever. yay
In the endThe effect is pretty subtle, but noticeable in game at 60fps and pretty nice. We actually added a black border to the screen For Design Reasons I'll get into later.
Here's what it looks like in game with our current rough world map:
Other notesThe
only reason this works is because we use a single large PNG as the background layer. We use this method for all of the area art - it doesn't blow up memory usage because we limit the layers to 3200x3200 pixels (RGBA) and deallocate the spritesheets from memory after leaving a given area. We also only use a few layers per map - thus each layer is at max, 42 MB or so - so memory-wise, this is a viable solution.
Anyways,
there is a drawback, of course, that being - we can't add any sort of animations directly on the map itself. E.g., animated water tiles or something. In theory it's possible - but it would be too expensive, memory wise.
However,
artistic limitations are good! They narrow down your solution space for a particular style, allowing you to refine faster.
In the next post, I'll get into all that game design stuff. This coding post ended up a lot longer than expected.