Problem with a scrolling tilemap

Started by
10 comments, last by Capitano 15 years, 8 months ago
Hello! first of all, I'm from Germany... and i apologize for my bad english. But I hope you understand what I mean:) My actual project is a 2D jump 'n run game with the help of python(and pygame). Everything works fine, until I included the scrolling of the tilemap. (I'm working with this tutorial(although it is a c++ tutorial) At first, i'm going to describe how my script works, and then what is not working. 1) The Tilemap is represented by a 2d-array. And the method that should fill the screen looks like this:
for y from 0-(scroll_y%TILESIZE) till ((height of the screen) plus (TILESIZE))
      for x from 0-(scroll_x%TILESIZE) till ((width of the screen) plus (TILESIZE))
         (step count in both loop is TILESIZE) 
          Tile with the coords[(x+scroll_x)/TILESIZE]and[(y+scroll_y) /TILESIZE] blit at the position(x,y)
    
scroll_y and scroll_x is the playerposition - the half of the screen(to get the upper left corner(there i start to draw the map)) -My method, that checks the collision of the player with the map(accessibility of the tiles..) works with the player-coordinates. Normally, this should be completely uninfluenced by the scrollcoordinates. Or is this wrong? In any case it seems not to work... 2)If I start the game, my player starts fairly in the upper left corner, and everything works fine, but when I walk in over the middle of the screen(from here is scrolled, because the player should be always centered in my game) everything is a bit shifted. Here is a screenshot: http://img253.imageshack.us/img253/2875/jumperenglishzm8.png You see, the player sprite is exact the 9px(scroll_y) to low(or the screen too high??..) I'm a bit confused..my problem looks trivial, but for 2 days, my project is frozen, because i agonise over this problem. So, my question: Has anyone recognised an error in reasoning in my text? Or perhaps someone recognizes a fault in my code?. In any cas I am at a loss. My complete code to download. If somebody needs some additional explanations to help me solving the problem, please feel free to ask in this thread. greetings from germany [Edited by - Capitano on July 29, 2008 6:21:21 AM]
Advertisement
from ... till looks more like Perl than Python to me, but oh well. :)

Collision detection should be done with world-coordinates, yes. Where you render things should not have any effect.


By replacing the following lines in Map.render:
for y in range(0-(scroll_y%TILESIZE),240+TILESIZE,TILESIZE):    for x in range(0-(scroll_x%TILESIZE),320+TILESIZE,TILESIZE):                self.mapImage.blit(self.tiles[self.mapArray[(x+scroll_x)/TILESIZE][(y+scroll_y)/TILESIZE]].get_gfx(),(x,y))

with:
for y in range(self.width):    for x in range(self.height):        self.mapImage.blit(self.tiles[self.mapArray[x][y]].get_gfx(), (x * TILESIZE, y * TILESIZE))

I verified that the collision checks are working properly. I then changed that to:
for y in range(scroll_y / TILESIZE, min(self.width, (scroll_y + 240) / TILESIZE) + 1):    for x in range(scroll_x / TILESIZE, min(self.height, (scroll_x + 320) / TILESIZE) + 1):        self.mapImage.blit(self.tiles[self.mapArray[x][y]].get_gfx(), (x * TILESIZE - scroll_x, y * TILESIZE - scroll_y))

so only visible tiles are drawn. Note that modulo takes the remainder of a division. When translating pixel offsets to tile indices, you'll want to use divisions instead. Ah well, the above code should fix the scrolling issues anyway.


Then I found out you're using a different approach for rendering the player, an approach that doesn't work well together with the camera offsets. the whole code is pretty mixed up it seems. You're rendering the map to a buffer and then to screen for no apparent reason and there's quite a few other odd things going on there.

I'll do some refactoring. I'll post the results later. :)
Create-ivity - a game development blog Mouseover for more information.
Quote:from ... till looks more like Perl than Python to me, but oh well. :)
:P


Quote:Ah well, the above code should fix the scrolling issues anyway.

Not really, but now i know, what my problem was(Yes past tense!!;))

I just had to convert the absolute world coordinates of the character before drawing in relative coordinates:
self.rect.topleft = (self.x-scroll_x,self.y-scroll_y)  
instead of
self.rect.topleft = (self.x,self.y)  

I knew that it will be something trivial, but that I've really not expected.
I was only focused on the rendering of the map and the collision detection, and not where i draw the player-sprite.

Quote:the whole code is pretty mixed up it seems.

Probably because I read a lot of tutorials in the last time. Of course written by different authors and different programming languages. I think this is the reason for my mix of programming techniques etc..(and of course my lack of experience)

Quote:I'll do some refactoring. I'll post the results later. :)

I'm anxious to your reply:)

greetings
Finally, here goes: Capitano_platformer (629 kB).

I first divided the game into several .py files: one for the map class, one for the player, one for the main game loop and one for utility functions. I then made the levels data-driven, so they're now stored as text files rather than as part of the code. I then implemented the player and tidied the animations up a bit. Then I added a camera variable, that is modified in the main game loop. Both the map and the player have a render function now, so they can draw their representation to the screen buffer. Finally, I redid the collision detection and handling - it should be slightly more robust, although I'm not entirely happy with it yet.


What I didn't do, but what would be very helpful, was writing a vector/point class, including overloaded operators to make working with them more natural. For example, position[0] + offset[0], position[1] + offset[1] is tedious, position + offset is much more natural.

I should also have written an animation system, so game objects can contain an animated sprite instead of having to implement their own animation code (which would mean code duplication).

As for the collision system, it's a bit tricky right now, with quite a few -1's and +1's sprawled across to make sure that being next to a tile doesn't count as a collision. Personally, I would probably generate a minimal number of bounding boxes from the grid and use those instead. I think box-box tests are more transparent than box-tile lookups and tests.


I hope it's useful to you somehow. If you have any further questions, feel free to ask. :)
Create-ivity - a game development blog Mouseover for more information.
Goede morgen! ;)

At first, i want to thank you for the refactoring.
Everything is more structured, logical and readable. And nearly 60fps faster!
Can you tell me the "brake" in my code?..(the screen buffer?)

Can you please explain what you mean with the vector/point class a little bit more?.. I think I doesn't exactly understand this point.

And another question:
I want to add a second layer on my tilemap, because my girlfrieds has made this two beautiful tiles:
http://capigame.bplaced.de/stuff/tile_ente.png
http://capigame.bplaced.de/stuff/tile_wiese.png
:D

My loop to render this 2nd layer looks like this(untested):
for y in range(max(camera[1] / self.tileheight, 0), min(self.height, (camera[1] + surface.get_height()) / self.tileheight + 1)):    for x in range(max(camera[0] / self.tilewidth, 0), min(self.width, (camera[0] + surface.get_width()) / self.tilewidth + 1)):        if self.tiles[self.grid[y][x]] != 2:  #2 = No tile-graphic            surface.blit(self.tiles[self.grid[y][x]], (x * self.tilewidth - camera[0], y * self.tileheight - camera[1]))

This loop runs after the rendering of the first layer.
Is there a better way to do this?


Why don't you use the pygame module "Sprite" in the Player-class?
I thought this is faster than blitting it "by hand"?

And a last question:
I want to add some objects like coins, exit-doors, enemies..
Should I create for every object an own class?..or one game_object class?


You see, I think my biggest problem is to structure the program in a reasonable way.

greetings!
from captain to captain ..

Du auch, gute Morgen. :)

A quick mockup of a 2D vector class:
class Vector2:    def __init__(self, x, y):        self.x = x        self.y = y    # +    def __add__(self, other):        return Vector2(self.x + other.x, self.y + other.y)    # -    def __sub__(self, other):        return Vector2(self.x - other.x, self.y - other.y)    # +=    def __iadd__(self, other):        self.x += other.x        self.y += other.y        return self    # -=    def __isub__(self, other):        self.x -= other.x        self.y -= other.y        return self    # for convenience, this function returns a string representation of the object    def __str__(self):        return '(' + str(self.x) + ', ' + str(self.y) + ')'

The above code allows us to do the following:
position = Vector2(10, 15)velocity = Vector(5, 0)position += velocityprint position

Much better than all the [0] and [1] in my current code, don't you think? There's one catch: most pygame drawing functions require a tuple or list of two elements for their positions. Fortunately, this behavior can also be emulated by overloading the __len__(self) and __getitem__(self, index) functions. Our vector can then act as if it were a 2-item list.



As for drawing multiple layers, I'd store the second layer in the level file as a separate block. I'd then load it into another grid - perhaps make the standard grid a list of grids (yuk, grid[layer][y][x]...) or build a Layer class and let the map contain a list of Layer instances (keep in mind which one you'll be using for the collision checks). Either way, you can reuse the same drawing code by looping through these grids.



I'm not using Sprites because I didn't look into them much - manually blitting does the job fine - but now that I'm looking into them, I'm not sure. I don't want to inherit game objects from a Sprite class, because their essence is not being a sprite, that's just their representation. What if an object needs multiple images to be displayed? :)

The Sprites and Groups system can be useful, I'm sure, but I should investigate it some more to see how well it fits in with my requirements. I'd probably still write my own Sprite class, because these seem to lack animation capabilities and that's a rather important thing for me.

As for being faster, they seem to employ some sort of dirty flagging system. Well, since the screen needs to be redrawn every frame anyway, that should hardly make a difference. On mobile phone games, it makes a lot of sense, but on PC's, not so much. Depends a bit on where the bottlenecks are in your game of course.



Coins and enemies would do well with a class of their own, yes. However, you may want to give them a common interface: an update() function for their logic and a render(camera, screen) function are what come to mind. You can then store them in a single list and run their update and render functions all in one loop.

If they have a lot in common with each other, then it makes sense to write a parent class for them that takes care of the shared functionality. That depends a bit on what exactly you want each of these elements to do.



Structuring depends a lot on experience. When I first started out, I would hack around a bit and see what works. Now, I have a fair idea of how to structure a game. Of course, I still run into areas that I'm not familiar with. Often, one of the first things I do, besides some research, is building a few prototypes to get a feeling for the subject.

/monolithic post ends here ;)

[Edited by - Captain P on July 31, 2008 4:39:26 AM]
Create-ivity - a game development blog Mouseover for more information.
I just found out there's a bug in my collision code: in the jumping part, the y value is determined by dividing the players position y component by the tile height, then subtracting one. That last part causes a problem: it checks one tile too high. Remove the - 1 at line 85 in player.py to solve the issue.

I'm in a Python mood these days, heh. ;)
Create-ivity - a game development blog Mouseover for more information.
Ok, the new render method:

for layer in range(2):    for y in range(max(camera[1] / self.tileheight, 0), min(self.height, (camera[1] + surface.get_height()) / self.tileheight + 1)):        for x in range(max(camera[0] / self.tilewidth, 0), min(self.width, (camera[0] + surface.get_width()) / self.tilewidth + 1)):            surface.blit(self.tiles[self.grid[layer][y][x]][0], (x * self.tilewidth - camera[0], y * self.tileheight - camera[1]))

And it works perfectly!


What do you mean with function overlading?
To write a new method with the same name, but different input arguments?

And a last question:
Where (and how) would you store information where enemies or objects for are?

My first idea was creating a third object-layer, but there is one big problem. I can't place object e.g. at (50,60)..only 32,64,96...


Is there a better reason to store the object information? (in the lvl.txt)

Quote:I'm in a Python mood these days, heh. ;)

There are a lot of more worse moods..:D

greetings


ps: Once again, I have to apologize for my bad english.. [rolleyes]

[Edited by - Capitano on August 1, 2008 4:19:31 PM]
Change the first line, for layer in range(2):, to for layer in range(len(self.tiles)):. While your line works, it will always iterate over exactly 2 layers. What if you add another layer? You'll have to change the 2 to a 3. By retrieving the number of layers, you ensure that you're always drawing all layers.

Quote:What do you mean with function overlading?
To write a new method with the same name, but different input arguments?

That's what function overloading is, yeah. In Python however, functions can't be overloaded. But operators can. Sometimes that's pretty useful.

Quote:And a last question:
Where would you store the information where the enemies for example are?
I have two ideas, but I am not very happy about them..:)
1) Create a third object-layer, but there is one big problem.
I can't place object e.g. at (50,60)..only 32,64,96...
2)create directly in the main.py for every enemy a new instance of the class Enemy with the arguments x,y.
But this isn't very flexible..

Is there a better reason to store the object information? (in the lvl.txt)

There's several issues here: where and how to store this information and how to handle it. I'd store them in the level files, although for testing purposes, there's nothing wrong with adding them to a list in main.py directly. It's usually fairly easy to modify that to a system that loads them from a file. That is, if you keep that possibility in mind beforehand.

Of course, that means you'll have to modify the level format a bit. I'd probably split each section up with an identifier line, to make parsing it a little easier:
TILEStile_sky.gif	non-solidtile_gras.gif	solidGRID111111100001100001111111ENTITIESplayer	55	55coin	120	80coin	140	80enemy	200	110

When your loading function reaches the ENTITIES section, it should check the first token of every line, and depending on that, create the associated object, using the second and third token as it's position:
# Strip a line of end-line characters, then split it into tokens,# by default it will split whenever it encounters white-space (spaces, tabs)(entity, x, y) = line.strip().split()if entity == 'player':	entities.append(Player(x, y))# and so on

Creating a layer for them doesn't sound like a good option - too restricting. Instead, create a list of entities and add them to that list. Then, every game cycle, call update() and render(camera, screen) on every entity in the list. An enemy would do some movement in it's update function, a coin would perhaps do nothing. All would render themselves in their render function.

Quote:ps: Once again, I have to apologize for my bad english.. [rolleyes]

It's pretty good actually. No need to apologize. :)
Create-ivity - a game development blog Mouseover for more information.
Quote:That's what function overloading is, yeah. In Python however, functions can't be overloaded. But operators can. Sometimes that's pretty useful.

Ok, i hope i've done it the right way:
    def __getitem__(self,index):        return [self.x,self.y][index]    def __len__(self):        return len([self.x,self.y])

I think it works:)

Quote:Instead, create a list of entities and add them to that list. Then, every game cycle, call update() and render(camera, screen) on every entity in the list.

That sounds good.


Quote:It's pretty good actually. No need to apologize. :)

I give you the phone number of my english teacher, and then...please repeat those two sentences..:D

greetings

I reply again, if i get a new problem..(give me 5-10minutes..heh)

This topic is closed to new replies.

Advertisement