Sign in to follow this  
Capitano

Problem with a scrolling tilemap

Recommended Posts

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 [url=http://jnrdev.72dpiarmy.com/en/jnrdev3/]this[/url] 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. [url=http://capigame.bplaced.de/stuff/jumper_english.rar]My complete code to download.[/url] 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]

Share this post


Link to post
Share on other sites
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. :)

Share this post


Link to post
Share on other sites
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

Share this post


Link to post
Share on other sites
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. :)

Share this post


Link to post
Share on other sites
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 ..

Share this post


Link to post
Share on other sites
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 += velocity
print 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]

Share this post


Link to post
Share on other sites
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. ;)

Share this post


Link to post
Share on other sites
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]

Share this post


Link to post
Share on other sites
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:
TILES
tile_sky.gif non-solid
tile_gras.gif solid
GRID
111111
100001
100001
111111
ENTITIES
player 55 55
coin 120 80
coin 140 80
enemy 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. :)

Share this post


Link to post
Share on other sites
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)

Share this post


Link to post
Share on other sites
Quote:
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:)

No, that doesn't look good. __getitem__ is called under the hood when you use the [] operator. In this case, if index is 0, it should return self.x, if index is 1, it should return self.y.

__len__ is called when you call len() on your object. It should return the number of elements inside your object, not a list of it's content. In this case, simply return 2.

What this boils down to is that your vector is acting as if it were a tuple or list. The reason I suggested it is because some Pygame functions want positions as a tuple. They check if the given argument contains exactly two items (which is why we need to implement __len__) and then they take the first element as the x component and the second element as the y component (which is why we need to implement __getitem__).

Of course, you can also write a GetTuple() function:
def GetTuple(self):
return (self.x, self.y)

and pass the result to those drawing functions. It's mostly a matter of personal preference.
Quote:
I give you the phone number of my english teacher, and then...please repeat those two sentences..:D

Writing and speaking are two different things. ;)

Share this post


Link to post
Share on other sites
Ok, my program grows..:)

This is the new version:
http://capitano.bplaced.de/dev/jumpnrun.rar

Still without the Vector class, but I think, it's not soo important. In any case..the script works.

What do you think about the way i continued the script.
(I know, that the code is very improvable..;) )

greetings

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this