You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDA瓦片碰撞检测算法在特定边界场景下返回错误碰撞数据的问题排查求助

DDA瓦片碰撞检测算法在特定边界场景下返回错误碰撞数据的问题排查求助

我现在遇到个头疼的问题:用DDA算法做基于瓦片的碰撞检测,目的是防止高速物体穿透碰撞体,但我写的算法在某些特定的边界场景下会返回完全无效的结果。这里需要说明的是,我的DDA算法是支持浮点坐标的。

下面是几个典型的错误案例:

案例1:垂直方向沿瓦片边界移动

比如一条从(1.0, 1.0)垂直向上到(1.0, 2.0)的DDA线,算法返回了错误结果:

Collision Position: (1.0, 1)
Tile Exited: (1, 1)
Tile Exited Side: 2
Tile Entered: (0, 0)
Tile Entered Side: 0

而正确的结果应该是这样的:

Collision Position: (1.0, 1)
Tile Exited: (1, 1)
Tile Exited Side: 2
Tile Entered: (1, 2)
Tile Entered Side: 0

(直观来看,这条线明明是从(1,1)瓦片的顶部出去,应该进入正上方的(1,2)瓦片,但算法却错误指向了(0,0))

案例2:增大移动距离后错误依旧

即使把移动距离拉大,结果还是不对。比如从(1,1.0)水平向右移动到更远位置时,算法返回:

Collision Position: (1, 1.0)
Tile Exited: (1, 1)
Tile Exited Side: 1

Tile Entered: (0, 0)
Tile Entered Side: 3

Collision Position: (2, 1.0)
Tile Exited: (2, 1)
Tile Exited Side: 1

Tile Entered: (1, 0)
Tile Entered Side: 3

(本来应该是从(1,1)右侧出去进入(2,1),结果算法却指向了(1,0),完全偏离方向)

案例3:对角线移动的部分错误

从(2, 2)到(3, 3)的对角线移动,算法虽然返回了经过的2个瓦片,但结果对应的是(2,2)附近的瓦片,而不是预期的(3,3)方向:

Collision Position: (2.0, 2)
Tile Exited: (1, 1)
Tile Exited Side: 2
Tile Entered: (2, 2)
Tile Entered Side: 0

Collision Position: (2, 2.0)
Tile Exited: (1, 1)
Tile Exited Side: 1
Tile Entered: (2, 2)
Tile Entered Side: 3

(这条线是向右上方走,本该进入(3,3)附近的瓦片,但算法却停留在(2,2))

正常场景表现

需要说明的是,当射线不在瓦片边界上,只是穿过瓦片边缘时,算法工作是正常的,只有在刚好沿瓦片边界移动时才会出问题。

问题代码

我的RayCast类代码如下:

class RayCast:
    def __init__(self, point1, point2):
        self.point1 = point1
        self.point2 = point2
        self.collision_data = []

        self.dx = point2[0] - point1[0]
        self.dy = point2[1] - point1[1]

        if self.dx == 0 and self.dy == 0:
            return

        self.x_step = 1 if self.dx > 0 else -1
        self.y_step = 1 if self.dy > 0 else -1

        self.next_vertical = ceil(point1[0]) if self.dx > 0 else floor(point1[0])
        self.next_horizontal = ceil(point1[1]) if self.dy > 0 else floor(point1[1])

        self.t_max_x = (self.next_vertical - point1[0]) / self.dx if self.dx != 0 else float('inf')
        self.t_max_y = (self.next_horizontal - point1[1]) / self.dy if self.dy != 0 else float('inf')
        self.t_delta_x = abs(1 / self.dx) if self.dx != 0 else float('inf')
        self.t_delta_y = abs(1 / self.dy) if self.dy != 0 else float('inf')

        self.x, self.y = point1

        while (self.x_step > 0 and self.x < point2[0]) or (self.x_step < 0 and self.x > point2[0]) or (
                self.y_step > 0 and self.y < point2[1]) or (self.y_step < 0 and self.y > point2[1]):
            if self.t_max_x < self.t_max_y:
                self.x = self.next_vertical
                self.y = point1[1] + self.t_max_x * self.dy
                self.t_max_x += self.t_delta_x
                self.next_vertical += self.x_step
                tile_exited_side = 3 if self.x_step < 0 else 1  # Left = 3, Right = 1
            else:
                self.y = self.next_horizontal
                self.x = point1[0] + self.t_max_y * self.dx
                self.t_max_y += self.t_delta_y
                self.next_horizontal += self.y_step
                tile_exited_side = 0 if self.y_step < 0 else 2  # Up = 0, Down = 2

            collision_x, collision_y = round(self.x, 10), round(self.y, 10)  # Avoid floating point precision issues

            if collision_x == int(collision_x) and collision_y == int(collision_y):  # Corner case
                if self.dx > 0 and self.dy > 0:
                    tile_exited = (int(collision_x) - 1, int(collision_y) - 1)
                    tile_entered = (tile_exited[0] + 1, tile_exited[1] + 1)
                elif self.dx < 0 and self.dy > 0:
                    tile_exited = (int(collision_x), int(collision_y) - 1)
                    tile_entered = (tile_exited[0] - 1, tile_exited[1] + 1)
                elif self.dx > 0 and self.dy < 0:
                    tile_exited = (int(collision_x) - 1, int(collision_y))
                    tile_entered = (tile_exited[0] + 1, tile_exited[1] - 1)
                else:
                    tile_exited = (int(collision_x), int(collision_y))
                    tile_entered = (tile_exited[0] - 1, tile_exited[1] - 1)

            elif collision_x == int(collision_x):  # Hit a vertical grid line
                tile_exited = (int(collision_x) - (1 if self.dx > 0 else 0), int(floor(self.y)))
                tile_entered = (tile_exited[0] + (1 if self.dx > 0 else -1), tile_exited[1])
            else:  # Hit a horizontal grid line
                tile_exited = (int(floor(self.x)), int(collision_y) - (1 if self.dy > 0 else 0))
                tile_entered = (tile_exited[0], tile_exited[1] + (1 if self.dy > 0 else -1))

            tile_entered_side = (tile_exited_side + 2) % 4  # Opposite side

            collision_data = RayCollisionData(
                collision_position=(self.x, self.y),
                tile_exited=tile_exited,
                tile_exited_side=tile_exited_side,
                tile_entered=tile_entered,
                tile_entered_side=tile_entered_side
            )
            self.collision_data.append(collision_data)

        # get rid of the last collision data as it is redundant
        self.collision_data.pop()

可视化测试工具

为了更直观地看问题,我还做了一个基于Pygame的可视化测试工具,代码如下:

class Editor:
    def __init__(self, screen: pygame.Surface, data):
        self.original_screen_size = screen.get_size()
        # resize the screen to 800x600
        self.screen = pygame.display.set_mode((800, 600), pygame.RESIZABLE) # TODO there may be a bug here
        self.data = data
        self.running = True
        self.manager = pygame_gui.UIManager((800, 600))
        self.clock = pygame.time.Clock()
        self.time_delta = 0.0
        self.top_panel = pygame_gui.elements.UIPanel(relative_rect=pygame.Rect((0, 0), (800, 50)),
                                                  manager=self.manager,
                                                      anchors={"top": "top", "left": "left", "right": "right"})
        self._right_panel_ui = pygame_gui.elements.UIPanel(relative_rect=pygame.Rect((600, 50), (200, 550)),
                                                        manager=self.manager,
                                                            anchors={"top": "top", "bottom": "bottom"})
        self.right_panel = pygame_gui.elements.UIScrollingContainer(relative_rect=pygame.Rect((0, 0), (200, 550)),
                                                   manager=self.manager,
                                                                    container=self._right_panel_ui)
        self.display_surface = pygame.Surface((600, 550))
        self.display_surface.fill((255, 255, 255))

    def run_editor(self):
        while self.running:
            self.time_delta = self.clock.tick(60) / 1000.0
            self.input()
            self.manager.update(self.time_delta)
            self.update()
            self.screen.fill((0, 0, 0))
            self.draw()
            self.screen.blit(self.display_surface, (0, 50))
            self.manager.draw_ui(self.screen)
            pygame.display.flip()
        return self.data

    def process_event(self, event: pygame.event.Event):
        pass

    def input(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.VIDEORESIZE:
                new_size = (event.w, event.h)
                print(f"new_size = {new_size}")
                self.screen = pygame.display.set_mode(new_size, pygame.RESIZABLE)
                self.manager.set_window_resolution(new_size)  # Update UI manager
                # resize the display surface
                self.display_surface = pygame.Surface((new_size[0] - 200, new_size[1] - 50))
                self.display_surface.fill((255, 255, 255))
                # move the right panel (ui) to the right side of the screen
                self._right_panel_ui.relative_rect = pygame.Rect((new_size[0] - 200, 50), (200, new_size[1] - 50))

            self.process_event(event)

            self.manager.process_events(event)

    def update(self):
        pass

    def draw(self):
        pass

class UIRaycastTester(Editor):
    def __init__(self, screen: pygame.Surface):
        super().__init__(screen, None)
        self.x1 = 0
        self.y1 = 0
        self.x2 = 10
        self.y2 = 10
        self.tile_size = 32

        # add an input for the x and y values
        self.x1_input = pygame_gui.elements.UITextEntryLine(relative_rect=pygame.Rect((10, 10), (100, 30)),
                                                            manager=self.manager)
        self.y1_input = pygame_gui.elements.UITextEntryLine(relative_rect=pygame.Rect((120, 10), (100, 30)),
                                                            manager=self.manager)

备注:内容来源于stack exchange,提问作者Ico Twilight

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 10:43:07