Source code for arcade.hitbox

"""
This module is used for calculating hit boxes
"""
import pymunk

from PIL import Image

from pymunk import autogeometry


[docs]def calculate_hit_box_points_simple(image): """ Given an image, this returns points that make up a hit box around it. Attempts to trim out transparent pixels. :param Image image: :Returns: List of points """ left_border = 0 good = True while good and left_border < image.width: for row in range(image.height): pos = (left_border, row) pixel = image.getpixel(pos) if type(pixel) is int or len(pixel) != 4: raise TypeError("Error, calculate_points called on image not in RGBA format") else: if pixel[3] != 0: good = False break if good: left_border += 1 right_border = image.width - 1 good = True while good and right_border > 0: for row in range(image.height): pos = (right_border, row) pixel = image.getpixel(pos) if pixel[3] != 0: good = False break if good: right_border -= 1 top_border = 0 good = True while good and top_border < image.height: for column in range(image.width): pos = (column, top_border) pixel = image.getpixel(pos) if pixel[3] != 0: good = False break if good: top_border += 1 bottom_border = image.height - 1 good = True while good and bottom_border > 0: for column in range(image.width): pos = (column, bottom_border) pixel = image.getpixel(pos) if pixel[3] != 0: good = False break if good: bottom_border -= 1 # If the image is empty, return an empty set if bottom_border == 0: return [] def _check_corner_offset(start_x, start_y, x_direction, y_direction): bad = False offset = 0 while not bad: y = start_y + (offset * y_direction) x = start_x for count in range(offset + 1): my_pixel = image.getpixel((x, y)) # print(f"({x}, {y}) = {pixel} | ", end="") if my_pixel[3] != 0: bad = True break y -= y_direction x += x_direction # print(f" - {bad}") if not bad: offset += 1 # print(f"offset: {offset}") return offset def _r(point, height, width): return point[0] - width / 2, (height - point[1]) - height / 2 top_left_corner_offset = _check_corner_offset(left_border, top_border, 1, 1) top_right_corner_offset = _check_corner_offset(right_border, top_border, -1, 1) bottom_left_corner_offset = _check_corner_offset(left_border, bottom_border, 1, -1) bottom_right_corner_offset = _check_corner_offset(right_border, bottom_border, -1, -1) p1 = left_border + top_left_corner_offset, top_border p2 = (right_border + 1) - top_right_corner_offset, top_border p3 = (right_border + 1), top_border + top_right_corner_offset p4 = (right_border + 1), (bottom_border + 1) - bottom_right_corner_offset p5 = (right_border + 1) - bottom_right_corner_offset, (bottom_border + 1) p6 = left_border + bottom_left_corner_offset, (bottom_border + 1) p7 = left_border, (bottom_border + 1) - bottom_left_corner_offset p8 = left_border, top_border + top_left_corner_offset result = [] h = image.height w = image.width result.append(_r(p7, h, w)) if bottom_left_corner_offset: result.append(_r(p6, h, w)) result.append(_r(p5, h, w)) if bottom_right_corner_offset: result.append(_r(p4, h, w)) result.append(_r(p3, h, w)) if top_right_corner_offset: result.append(_r(p2, h, w)) result.append(_r(p1, h, w)) if top_left_corner_offset: result.append(_r(p8, h, w)) # Remove duplicates result = tuple(dict.fromkeys(result)) return result
[docs]def calculate_hit_box_points_detailed(image: Image, hit_box_detail: float = 4.5): """ Given an image, this returns points that make up a hit box around it. Attempts to trim out transparent pixels. :param Image image: Image get hit box from. :param int hit_box_detail: How detailed to make the hit box. There's a trade-off in number of points vs. accuracy. :Returns: List of points """ def sample_func(sample_point): """ Method used to sample image. """ if sample_point[0] < 0 \ or sample_point[1] < 0 \ or sample_point[0] >= image.width \ or sample_point[1] >= image.height: return 0 point_tuple = sample_point[0], sample_point[1] color = image.getpixel(point_tuple) if color[3] > 0: return 255 else: return 0 # Do a quick check if it is a full tile p1 = 0, 0 p2 = 0, image.height - 1 p3 = image.width - 1, image.height - 1 p4 = image.width - 1, 0 if sample_func(p1) and sample_func(p2) and sample_func(p3) and sample_func(p4): # Do a quick check if it is a full tile p1 = (-image.width / 2, -image.height / 2) p2 = (image.width / 2, -image.height / 2) p3 = (image.width / 2, image.height / 2) p4 = (-image.width / 2, image.height / 2) return p1, p2, p3, p4 # Get the bounding box logo_bb = pymunk.BB(-1, -1, image.width, image.height) # Set of lines that trace the image line_set = pymunk.autogeometry.PolylineSet() # How often to sample? downres = 1 horizontal_samples = int(image.width / downres) vertical_samples = int(image.height / downres) # Run the trace # Get back one or more sets of lines covering stuff. line_sets = pymunk.autogeometry.march_soft( logo_bb, horizontal_samples, vertical_samples, 99, sample_func) if len(line_sets) == 0: return [] selected_line_set = line_sets[0] selected_range = None if len(line_set) > 1: # We have more than one line set. Try and find one that covers most of # the sprite. for line in line_set: min_x = None min_y = None max_x = None max_y = None for point in line: if min_x is None or point.x < min_x: min_x = point.x if max_x is None or point.x > max_x: max_x = point.x if min_y is None or point.y < min_y: min_y = point.y if max_y is None or point.y > max_y: max_y = point.y if min_x is None or max_x is None or min_y is None or max_y is None: raise ValueError("No points in bounding box.") my_range = max_x - min_x + max_y + min_y if selected_range is None or my_range > selected_range: selected_range = my_range selected_line_set = line # Reduce number of vertices # original_points = len(selected_line_set) selected_line_set = pymunk.autogeometry.simplify_curves(selected_line_set, hit_box_detail) # downsampled_points = len(selected_line_set) # Convert to normal points, offset fo 0,0 is center, flip the y hh = image.height / 2 hw = image.width / 2 points = [] for vec2 in selected_line_set: point = round(vec2.x - hw), round(image.height - (vec2.y - hh) - image.height) points.append(point) if len(points) > 1 and points[0] == points[-1]: points.pop() # print(f"{sprite.texture.name} Line-sets={len(line_set)}, Original points={original_points}, Downsampled points={downsampled_points}") return points