1

My English sucks, I tried to write this question my best :D

I'm making a 2D game with Pyglet, I need to make a tiled map. 1 tile = 50pixels x 50pixels But this takes to much PC power when I draw map 50x50 tiles with 20 enemies my fps drop from 60fps to 10fps Every tile and enemy is drawn in batch. what can I do to get my game more efficient?

I tried to scale tiles, but I get a black border on every tile and I want to have 50x50 pixels tile, not 50x50/scale

#Create blocks 50x50 pixels and replace with image names to get what i see.
from pyglet.window import key, FPSDisplay
import pyglet
import math

Background = pyglet.graphics.OrderedGroup(0)
Walls_Group = pyglet.graphics.OrderedGroup(1)

def preload_image(image):
    img = pyglet.image.load('images/' + image)
    return img

map_x = 50
map_y = 50
window_X = 1500
window_Y = 900

class GameWindow(pyglet.window.Window):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_location(100, 30)
        self.frame_rate = 1.0 / 100.0
        self.fps_display = FPSDisplay(self)
        self.fps_display.label.font_size = 30
        self.player_speed = 550
        self.right = False
        self.left = False
        self.up = False
        self.down = False
        self.map_load = False
        self.Walls_load = False
        self.map_scale = 1
        self.wall = preload_image('block.png')
        self.wall_list = []
        self.map_1_list = []
        self.sprite = preload_image('Grass_Green.png')
        self.main_batch = pyglet.graphics.Batch()

    def Mapka(self, x_size, y_size):

        for Y in range(y_size):
            for X in range(x_size):
                self.map_1_list.append(pyglet.sprite.Sprite(self.sprite, x=X * (self.sprite.width*self.map_scale), y=Y * (self.sprite.height*self.map_scale), batch=self.main_batch, group=Background))

        for i in self.map_1_list:
            i.scale = self.map_scale


    def Walls(self):
        self.times = math.ceil(map_x * (self.sprite.width*self.map_scale) / self.wall.width)  # Oblicza ilość ścian na dolnej części
        # mapy z zaokrągleniem

        self.times_y = math.ceil(map_y * (self.sprite.height*self.map_scale) / self.wall.height)

        for x in range(int(self.times)):
            self.wall_list.append(
                pyglet.sprite.Sprite(self.wall, x=x * self.wall.width, y=0, batch=self.main_batch, group=Walls_Group))
            self.wall_list.append(
                pyglet.sprite.Sprite(self.wall, x=x * self.wall.width, y=(self.times_y - 1) * self.wall.height,
                                     batch=self.main_batch, group=Walls_Group))
        for y in range(int(self.times_y)):
            self.wall_list.append(
                pyglet.sprite.Sprite(self.wall, x=0, y=y * self.wall.height, batch=self.main_batch, group=Walls_Group))
            self.wall_list.append(pyglet.sprite.Sprite(self.wall, x=self.times * self.wall.width - self.wall.width,
                                                       y=y * self.wall.height, batch=self.main_batch,
                                                       group=Walls_Group))

    def on_draw(self):
        self.clear()
        if not self.map_load:
            self.Mapka(map_x, map_y)
            self.map_load = True
        if not self.Walls_load:
            self.Walls()
            self.Walls_load = True
        self.main_batch.draw()
        self.fps_display.draw()

    def on_key_press(self, symbol, modifiers):
        if symbol == key.D:
            self.right = True
        if symbol == key.A:
            self.left = True
        if symbol == key.W:
            self.up = True
        if symbol == key.S:
            self.down = True
        if symbol == key.ESCAPE:
            pyglet.app.exit()

    def on_key_release(self, symbol, modifiers):
        if symbol == key.D:
            self.right = False
        if symbol == key.A:
            self.left = False
        if symbol == key.W:
            self.up = False
        if symbol == key.S:
            self.down = False

    def update_space(self, dt):
        for space in self.map_1_list:
            space.update()
            space.y -= 0 * dt
            if self.right:
                space.x -= self.player_speed * dt
            if self.left:
                space.x += self.player_speed * dt
            if self.up:
                space.y -= self.player_speed * dt
            if self.down:
                space.y += self.player_speed * dt

    def update_wall(self, dt):
        for wall in self.wall_list:
            wall.update()
            if self.right:
                wall.x -= self.player_speed * dt
            if self.left:
                wall.x += self.player_speed * dt
            if self.up:
                wall.y -= self.player_speed * dt
            if self.down:
                wall.y += self.player_speed * dt

    def update(self, dt):
        self.update_wall(dt)
        self.update_space(dt)

if __name__ == "__main__":
    window = GameWindow(window_X, window_Y, "Gra", resizable=False)
    pyglet.clock.schedule_interval(window.update, window.frame_rate)
    pyglet.app.run()

I want to have tiled map with minimum 100 x 100 tiles that can work on 60 fps. Tile = 50x50 pixels If there is a possibility to draw tiles but only the ones that are visible on screen, no further than screen X, Y.

Okto
  • 48
  • 3
  • We would need to at least see three things, 1) How do you create these tiles and 2) How do you update their positions and 3) How do you render them? If you could cut out just those three things from your code - it would help us quite a lot. I think I know what the problem is, but I'd like to see those pieces of code first :) – Torxed Feb 06 '19 at 20:20
  • 1
    I add the code you ask for. – Okto Feb 07 '19 at 00:18
  • Okto, I would just like to thank you for giving us code that is exactly what we want users to post. It's minimal, it's executable and it shows the problem! Great work writing a great question - and I hope the solution below works for you. Best of luck to your project! :) *(And your English is perfectly fine)* – Torxed Feb 08 '19 at 19:57

1 Answers1

0

Ok, so the main issue here is probably schedule_interval which doesn't operate as fast as possible. Mainly because it updates the scene and causes a render to trigger. The second being the wall.update() call (which I don't know exactly why you're calling) is extremely slow and you're doing it width*height^2 times. And it's highly inefficient.

There's a quick workaround for it. And below is a proposed way of doing it.

#Create blocks 50x50 pixels and replace with image names to get what i see.
from pyglet.window import key, FPSDisplay
import pyglet
import math
import time

Background = pyglet.graphics.OrderedGroup(0)
Walls_Group = pyglet.graphics.OrderedGroup(1)

def preload_image(image):
    img = pyglet.image.load('images/' + image)
    return img

map_x = 50
map_y = 50
window_X = 1500
window_Y = 900

class GameWindow(pyglet.window.Window):

    def __init__(self, *args, **kwargs):
        super(GameWindow, self).__init__(*args, **kwargs)
        self.set_location(100, 30)
        self.frame_rate = 1.0 / 100.0
        self.fps_display = FPSDisplay(self)
        self.fps_display.label.font_size = 30
        self.player_speed = 550
        self.right = False
        self.left = False
        self.up = False
        self.down = False
        self.map_load = False
        self.Walls_load = False
        self.map_scale = 1
        self.wall = preload_image('block.png')
        self.wall_list = []
        self.map_1_list = []
        self.sprite = preload_image('Grass_Green.jpg')
        self.main_batch = pyglet.graphics.Batch()
        self.alive = True
        self.last_scheduled_update = time.time()

        if not self.map_load:
            self.Mapka(map_x, map_y)
            self.map_load = True
        if not self.Walls_load:
            self.Walls()
            self.Walls_load = True

    def Mapka(self, x_size, y_size):

        for Y in range(y_size):
            for X in range(x_size):
                self.map_1_list.append(pyglet.sprite.Sprite(self.sprite, x=X * (self.sprite.width*self.map_scale), y=Y * (self.sprite.height*self.map_scale), batch=self.main_batch, group=Background))

        for i in self.map_1_list:
            i.scale = self.map_scale

    def Walls(self):
        self.times = math.ceil(map_x * (self.sprite.width*self.map_scale) / self.wall.width)  # Oblicza ilość ścian na dolnej części
        # mapy z zaokrągleniem

        self.times_y = math.ceil(map_y * (self.sprite.height*self.map_scale) / self.wall.height)

        for x in range(int(self.times)):
            self.wall_list.append(
                pyglet.sprite.Sprite(self.wall, x=x * self.wall.width, y=0, batch=self.main_batch, group=Walls_Group))
            self.wall_list.append(
                pyglet.sprite.Sprite(self.wall, x=x * self.wall.width, y=(self.times_y - 1) * self.wall.height,
                                     batch=self.main_batch, group=Walls_Group))
        for y in range(int(self.times_y)):
            self.wall_list.append(
                pyglet.sprite.Sprite(self.wall, x=0, y=y * self.wall.height, batch=self.main_batch, group=Walls_Group))
            self.wall_list.append(pyglet.sprite.Sprite(self.wall, x=self.times * self.wall.width - self.wall.width,
                                                       y=y * self.wall.height, batch=self.main_batch,
                                                       group=Walls_Group))

    def render(self):
        self.clear()
        self.main_batch.draw()
        self.fps_display.draw()
        self.flip()

    def on_key_press(self, symbol, modifiers):
        if symbol == key.D:
            self.right = True
        if symbol == key.A:
            self.left = True
        if symbol == key.W:
            self.up = True
        if symbol == key.S:
            self.down = True
        if symbol == key.ESCAPE: # [ESC]
            self.alive = 0
        self.update(1)

    def on_key_release(self, symbol, modifiers):
        if symbol == key.D:
            self.right = False
        if symbol == key.A:
            self.left = False
        if symbol == key.W:
            self.up = False
        if symbol == key.S:
            self.down = False

    def update_space(self, dt):
        for space in self.map_1_list:
            space.update()
            space.y -= 0 * dt
            if self.right:
                space.x -= self.player_speed * dt
            if self.left:
                space.x += self.player_speed * dt
            if self.up:
                space.y -= self.player_speed * dt
            if self.down:
                space.y += self.player_speed * dt

    def update_wall(self, dt):
        for wall in self.wall_list:
            wall.update()
            if self.right:
                wall.x -= self.player_speed * dt
            if self.left:
                wall.x += self.player_speed * dt
            if self.up:
                wall.y -= self.player_speed * dt
            if self.down:
                wall.y += self.player_speed * dt

    def update(self, dt):
        self.update_wall(dt)
        self.update_space(dt)

    def run(self):
        while self.alive == 1:
            if time.time() - self.last_scheduled_update > 0.25:
                self.update(time.time() - self.last_scheduled_update)
                self.last_scheduled_update = time.time()
            self.render()

            # -----------> This is key <----------
            # This is what replaces pyglet.app.run()
            # but is required for the GUI to not freeze
            #
            event = self.dispatch_events()

if __name__ == "__main__":
    window = GameWindow(window_X, window_Y, "Gra", resizable=False)
    window.run()

It's a minimal change to the code, but you change the built in scheduler with your own event loop, you're in charge of the timing and criteria for running self.update(). In this case, it's run every 0.25 seconds at a interval and after each on_keypress is triggered.

There's no reason to update sprites positions unless you move around. Or in the future, when enemies/objects performs actions.

I get a steady 144 FPS with this code.
But there's more to do here.. so I'll leave a long lasting tip which helps you solve future bottle necks. And I'll demonstrate how you'll use it.

Running the above commands, will give you something like this:

enter image description here

And this clearly show's the main cycle hogger as _wait_vsync.
Which made me realize I forgot to check the most obvious thing being the issue here. And that's that you and I forgot vsync=False to the window object.

The solution here is:

window = GameWindow(window_X, window_Y, "Gra", resizable=False, vsync=False)

Which gives me:

enter image description here

Yes, that's above 2700 FPS.
And the call-stack looks at lot more even:

enter image description here

From here on now, here's where the real challenges begin.
Any optimizations done will be minor, hard to find and a challenge to debug.

Hopefully you have the tools and understand of where and why to look at places :)

Torxed
  • 22,866
  • 14
  • 82
  • 131