Skip to main content
Tweeted twitter.com/StackGameDev/status/1193906381755162629
Source Link

Batch z-ordering problem in Cocos2d (Python)

I have a weird problem with my hexmap drawing code.

My code uses "squished" hexagons to achieve a pseudo-isometric view. Because of that, the order of drawing them on screen matters a lot.

Thankfully, I'm using cubic coordinates, so I can pass one of them as the z-level of the sprite in question. I've put it in code, and it worked...almost. The problem is that the program sometimes produces anomalous (and ugly) results, in spite of having the exact same input. To me it looks like some of the sprites ignore the z-level i set for them.

Take a look:

For comparison Working as intended on the left, and broken on the right.

As of now, I have no idea which factors contribute to this error - three consecutive script launches can have any combination of good and bad results.

Here is a barebones version of my code. I stripped it of all unneccessary parts, and left only the crucial elements. It is based on the Cocos2d(Python) library. The hexmap is stored as a OrderedDict with Hex(q,r,s) tuples as keys. Prior to drawing, all sprites are packed into a batch with appropriate z-level. Pressing any mouse button forces the program to redraw the map (this always removes the problem - until next launch).

import os
os.environ['PYGLET_SHADOW_WINDOW']="0"
import pyglet
import cocos
import collections
import math

Hex = collections.namedtuple("Hex", ["q", "r", "s"])
Point = collections.namedtuple("Point", ["x", "y"])
Orientation = collections.namedtuple("Orientation", ["f0", "f1", "f2", "f3", "b0", "b1", "b2", "b3", "start_angle"])
Layout = collections.namedtuple("Layout", ["orientation", "size", "origin"])
      
def generate_map(radius): # Here we generate the map
    map = collections.OrderedDict()
    for q in range (-radius, radius + 1):
        r1 = max(-radius, -q - radius)
        r2 = min (radius, -q + radius)
        for r in range (r1,r2 + 1):
            tile_type = 0
            key = Hex(q,r, -q-r)
            map[key] = {"type":tile_type}
    return map
            
def hex_to_pixel(layout, hex): # Map-to-screen coordinate conversion
    Matrix = layout.orientation
    size = layout.size
    origin = layout.origin
    x = (Matrix.f0 * hex.q + Matrix.f1 * hex.r) * size.x
    y = (Matrix.f2 * hex.q + Matrix.f3 * hex.r) * size.y
    return Point(x,y)            
            
def pointy_layout_size(sprite_width, sprite_height): # Some hex math
    layout_size_y = sprite_height
    layout_size_x = sprite_width/math.sqrt(3)
    return Point(layout_size_x, layout_size_y)            
            
            
sprite_width = 64
sprite_height = 26
layout_size = pointy_layout_size(sprite_width, sprite_height)
layout_pointy = Orientation(math.sqrt(3.0), math.sqrt(3.0) / 2.0, 0.0, 3.0 / 2.0, math.sqrt(3.0) / 3.0, -1.0 / 3.0, 0.0, 2.0 / 3.0, 0.5)
layout = Layout(layout_pointy, layout_size, Point(400,300))

pyglet.resource.path = [os.path.dirname(os.path.realpath(__file__)) + '\\']
pyglet.resource.reindex()
            
class Map_Layer(cocos.layer.Layer):
    is_event_handler = True

    def __init__(self):
        super(Map_Layer, self).__init__()         
               
        self.map_sprites_batch = cocos.batch.BatchNode()
        self.map_sprites_batch.position = layout.origin.x, layout.origin.y
        
    def batch_map(self): # Here we pack the sprites into the batch
        for hex, properties in map.items():
            point = hex_to_pixel (layout, hex)
            offset_anchor = sprite_width/2, sprite_height/2
            sprite = cocos.sprite.Sprite("hills.png", position = point, anchor = offset_anchor)
            self.map_sprites_batch.add(sprite, z = - hex.r)  # This line determines the z-level of every sprite inside the batch
        
        self.add(self.map_sprites_batch)    
    
    def on_mouse_press(self, x, y, buttons, mod):  # Force redraw on mouse click.
        for z, child in self.map_sprites_batch.children:
            self.map_sprites_batch.remove(child)
        self.batch_map()

cocos.director.director.init(800,600)

map = generate_map(5)  # The argument here determines map radius
layer = Map_Layer()
scene = cocos.scene.Scene(layer)
layer.batch_map()
cocos.director.director.run (scene)

Here is the tile image: Don't forget to rename it to hills.png

I don't know what causes it - could be my own faulty code, wrong usage of library functions, problems in the library itself - or something else entirely. I will appreciate any help in solving this problem.