Tile-based landscape frenzy!
On the First day, Niriel created the Entities. And the Entities were floating in the empty void. And Niriel got bored and said “Let there be tiles all over the place”. And there were tiles, and the Entities could walk on the tiles, and everybody was pretty much happy, except Entity number 3 who’s always complaining.
- Go there : https://github.com/Niriel/Infiniworld,
- click on the big gray “Downloads” button (right of the page),
- choose “v0.0.3”.
Many many keys !
- Enter/Return: create a new entity.
- ; and ' (semicolon and quote): take control the previous/next entity
- \ (backslash): create a new area. But it’s useless in this version since the areas created that way don’t have tiles.
- [ and ] (left and right square brackets): move to the previous or the next area. Note that the game starts in the area None, which means that no area is displayed. You must press [ or ] to start seeing something.
- . and / (period and slash): teleport the selected entity to the previous or the next area.
- W, A, S and D: move the selected entity. Note that diagonal movements are allowed, and in that case you are not perfectly lined up with the tiles: you moved by square root of two in x and y.
- Esc: quit. You can also click on the “close” button of the game window.
The controls are not awesome yet. They’re hard-coded for a US keyboard, and not even US international: the quote key doesn’t work for me because it waits for another key after that in order to put an accent on it. But hey, that’s a start.
The little new details.
- The window has a title now. I think there may be a bug in my version of Ubuntu preventing me to set the title after creating the window (it only changes the name in the task bar). So I set the title before creating the window and hop, it works.
- FPS counter. Since the game loop authorizes a max of 60 FPS, that’s what I get. Actually the picture shows 59 FPS, it jitters between 60 and 59. Maybe because of the inaccuracy of time.sleep?
The big things.
There are two major modifications to the code:
- The world can have several areas.
- Areas have tiles.
When you run this demo, a world is generated. It contains three areas. Each area contains 16384 tiles (128 by 128). I chose this relatively high number to challenge myself and make sure I could render the area fast enough. If I couldn’t, I would give up on pygame and python. Fortunately I am still in my 60 FPS budget which makes me very happy. My first two implementations crashed me down to 5 FPS:
- The first implementation was the straight-forward brute force: “Ask pygame to display everything, pygame is smart enough to realize it has nothing to do 95 % of the time, right ?”. Actually it wasn’t that bad on small maps, as this method was faster than my second implementation.
- The second implementation just added a line to the first one with a little test in it: “For each tile, check if it intersects the view, and if it does then draw it”. The result was horrendous since I was processing 16k tiles. Looks like pygame.Rect.colliderect() is not super fast since I got a better performance without checking for smaller maps.
- I threw that away. Since I know in advance the min and max world coordinates of my View, I just have to look for the tiles that are within these coordinates. And it’s fast, and scales well. I could have done that in the first place but it’s a bit more code to write and maybe the other methods would have been okay.
Python does not have two-dimensional arrays so implementing the TileMap was not straightforward. The first idea that comes to mind is to use a list of lists. I didn’t like it because:
- it is a bit hard to guarantee that all the lists have the same length;
- it’s a mess to resize, you have to extend or shrink and shift every list;
- I don’t like to write my coordinates between brackets like tiles[x][y];
- I cannot have negative indices ! Negative indices will probably be very nice to have when the world is procedurally generated, as I do not want to impose arbitrary barriers on two sides of my map.
And then I realized we’re in the marvelous world of Python and that we do not actually need rigid two-dimensional arrays when we have dictionaries and hashable tuples! So a tile map is stored in a dictionary: keys are (x, y) tuples. It is fast, and has none of the drawbacks listed above. Sold !
The TileModel object is very simple, it just contains a nature for the floor, and a height for said floor. If the height is 0 it means you could walk on it. If it’s 1, it means it’s raised, like a cliff, and you can’t go. I should have named it “elevation” instead of height.It is not a 3D game, and I must resist the temptation of allowing the player to stand on high tiles. That will be for the next game. Also, tiles don’t block the movements of the entities yet, since there is no physics engine; the entities are even free to run out of the map.
Next step: Physics !
Stay tuned to learn how I include string theory into the physics engine!