YoYoGames / GameMaker-Bugs

Public tracking for GameMaker bugs
24 stars 8 forks source link

The mask of place_meeting is incorrect #5098

Open UltraDrone opened 7 months ago

UltraDrone commented 7 months ago

Description

place_meeting在游戏中存在的bug——遮罩边缘被限定在像素中间

A bug in the game with place_meeting--The mask edge is limited to the middle of the pixel

摘要: 本文通过放大窗口并可视化精灵的碰撞遮罩,发现无论遮罩类型是什么,place_meeting 的遮罩边缘都会被限定在x.5px的位置①。这种情况下当物体进行浮点数移动时,物体可能会在重叠后仍未检测到碰撞。虽然此bug对碰撞遮罩的影响不超过一个像素的厚度,但是本文作者强烈希望yoyo能重视此问题并正确修复此bug②。

Abstract: In this paper, by enlarging the window and visualizing the collision mask of the Sprite, we find that regardless of the mask type, mask edge of place_meeting is restricted to the x.5 pixel①. In this case, when the object is moving to fractional coordinate, it may overlaps with other objects and still can not detect the collision. Although the impact of this bug on the collision mask is no more than one pixel thick, the author of this article strongly hopes that yoyo will take this issue seriously and fix the bug correctly②.

注①:这里指的是place_meeting 的检测到的上下左右边界总是(整数 + 0.5)px。

注②:本文使用的GM的IDE版本是beta v2024.400.0.516,runtime版本是 v2024.400.0.537

Note ①: What this means is that the detected upper, bottom, left, and right boundaries of place_meeting are always (integer + 0.5) px.

Note ②: The GM IDE version used in this article is beta v2024.400.0.516 and runtime version v2024.400.0.537.

目录

1. 前言 Introduction

我是一个中国人,并不擅长英文,因此将中文原文与机翻译文放在一起,若出现译文表述不清的情况,请以中文原文为准。事情是这样的,前些时间YYG在四月beta中修复了一些关于collision函数的bug,这使得collision函数在游戏中更加好用了。但是我发现在四月beta中place_meeting函数仍存在bug。为了帮助YoYo修复这一bug,我设计了科学合理的debug方案让YoYo理解bug的详细特征。

As a Chinese, I am not good at English, so I put the original Chinese text together with the machine translation. If the translation is not clear, the original Chinese text shall prevail. The thing is, YYG fixed some bugs with the collision function in the April beta a while back, which made the collision function more usable in the game. But I found place_meeting function still have a bug in the April beta. In order to help YoYo fix this bug, I designed a scientific and reasonable debug scheme so that YoYo could understand the detailed features of the bug.

2. 设计方案 Design Scheme

碰撞问题最好的debug办法就是可视化碰撞遮罩。我们只需要遍历屏幕上所有的像素点,让其对目标精灵做点碰撞测试,然后将所有与精灵碰撞到的点绘制在相应位置即可。由于我们研究的是亚像素级别的误差,必须放大视野才能看清所有细节。综上,想要看清楚实际运行时的真实碰撞遮罩需要做到以下两点:

  1. 正确放大视野,并且能在正确的位置绘制精灵与边界框。
  2. 在正确的位置检测碰撞,并正确地绘制检测到的碰撞结果。

The best debugging method for collision problems is to visualize collision masks. We just need to traverse all the pixels on the screen, have them perform point collision tests on the target sprite, and then draw all the points that collide with the sprite at the corresponding positions. Since we are studying sub-pixel level errors, it is necessary to enlarge the window to see all details clearly. In summary, to see the actual collision mask during game clearly, the following two points need to be achieved:

  1. Correctly magnify the window and be able to draw sprites and bounding boxes in the correct positions.
  2. Detect collisions in the correct positions and draw the detected collision results correctly.

另外,为了证明place_meeting确实存在bug,我们需要一个正确的案例作为对照组,从而进行对照实验。因为GM中的物理是基于box2d的,而box2d作为一个历史悠久的开源库,我们可以假定他是正确的,而GM中物理检测碰撞的效果也确实比place_meeting更为精准,所以我们使用phsics_test_overlap作为对照组进行比对。

In addition, in order to prove that place_meeting does have bugs, we need a correct case as a control group, so as to conduct a controlled experiment. Since the physics in GM is based on box2d, and box2d is an open source library with a long history, we can assume that he is correct, and the physics in GM does acutally detect collisions more accurately than place_meeting. So we used phsics_test_overlap as the control group for comparison.

下面的测试工程为一种可行的解决方案。为了让大家直观地感受到bug的真实性并复现此bug,本人强烈建议大家按照下面的工程自己实践一下。这样才能深刻领悟本文所描述的问题。

The following testing project is a feasible solution. In order to make everyone intuitively feel the authenticity of the bug and reproduce it, I strongly recommend that you practice it yourself according to the following project. Only in this way can we deeply understand the problem described in this article.

3. 测试工程 Testing Project

在测试工程中,我们需要如下资产:

In the test project, we need the following assets:

place_bug_pic1

3.1. 相机设置 Camera Setting

为了方便我们在运行时任意放大界面观察细节,我们新建一个obj_camera_control。它的创建事件和步事件的代码如下:

To make it easier for us to zoom in and see the details at runtime, we create a new obj_camera_control. The code for its create event and step event is as follows:

创建事件:

Create Event:

max_scale = 32;
min_scale = 0.03125;
//camera scales

scale_change_times = 2;
view_enabled = true;
view_visible[0] = true;

camera_destroy(view_camera[0]);
view_camera[0] = camera_create_view(0, 0, room_width, room_height);
camera_apply(view_camera[0]);

orign_cw = camera_get_view_width(view_camera[0]);
//orign_ch = camera_get_view_height(view_camera[0]);

t_mx = mouse_x;
t_my = mouse_y;

t_cx = camera_get_view_x(view_camera[0]);
t_cy = camera_get_view_y(view_camera[0]);

window_set_size(room_width * 20, room_height * 20);
display_set_gui_size(room_width * 10, room_height * 10);

步事件:

Step Event:

var cx = camera_get_view_x(view_camera[0]);
var cy = camera_get_view_y(view_camera[0]);
var cw = camera_get_view_width(view_camera[0]);
var ch = camera_get_view_height(view_camera[0]);
var mx = mouse_x;
var my = mouse_y;

if(mouse_wheel_up()){
    if(cw / orign_cw > min_scale){
        camera_set_view_size(view_camera[0], cw / scale_change_times, ch / scale_change_times);
        camera_set_view_pos(view_camera[0], mx - (mx - cx) / scale_change_times, my - (my - cy) / scale_change_times);
        camera_apply(view_camera[0]);
    }
}

if(mouse_wheel_down()){
    if(cw / orign_cw < max_scale){
        camera_set_view_size(view_camera[0], cw * scale_change_times, ch * scale_change_times);
        camera_set_view_pos(view_camera[0], mx - (mx - cx) * scale_change_times, my - (my - cy) * scale_change_times);
        camera_apply(view_camera[0]);
    }
}

if(mouse_check_button_pressed(mb_middle)){
    t_mx = mouse_x;
    t_my = mouse_y;

    t_cx = camera_get_view_x(view_camera[0]);
    t_cy = camera_get_view_y(view_camera[0]);
}

if(mouse_check_button(mb_middle)){
    if(t_mx != mx || t_my != my){
        camera_set_view_pos(view_camera[0], cx - mx + t_mx, cy - my + t_my);
        camera_apply(view_camera[0]);
    }
}

3.2. 网格绘制 Grid Rendering

为了方便我们在运行时观察小数坐标下的遮罩,我们新建一个obj_grid。它的绘制事件的代码如下:

To make it easier for us to see the mask in decimal coordinates at run time, we create a new obj_grid. Its code for drawing events is as follows:

绘制事件:

Draw Event:

draw_set_alpha(0.5);
draw_primitive_begin(pr_linelist);
for(var i = 0; i <= room_width; i++){
    draw_vertex(i, 0);
    draw_vertex(i, room_height);
}
for(var i = 0; i <= room_height; i++){
    draw_vertex(0, i);
    draw_vertex(room_width, i);
}
draw_primitive_end();
draw_set_alpha(1);

3.3. 物体设置 Object Setting

为了测试遮罩,我们需要设置墙体作为碰撞目标。

To test the mask, we need to set the walls as collision targets.

为了能直观观察遮罩与物体的关系,我们新建一个8px*8px的spr_wall,设置它的原点坐标为中心,且尽可能使它的每个像素点都不太相同,然后设置他的碰撞遮罩类型为旋转的矩形

In order to visualize the relationship between the mask and the object, we create a spr_wall with 8px * 8px, set its origin coordinate as the MiddleCenter, and make every pixel of it as different as possible, and set its collision mask type to Rectangle with rotatiion.

place_bug_pic2

由于我们要对physics_test_overlapplace_meeting同时进行测试,所以我们新建两个object分别叫做obj_wall_phisicsobj_wall_place,它们均绑定spr_wall作为精灵。

Since we want to test physics_test_overlap and place_meeting at the same time, we need to create two new objects called obj_wall_phisics and obj_wall_place. They all bind spr_wall as their sprites.

为了使obj_wall_phisics能够使用物理,我们需要勾选Uses Physics

To enable obj_wall_phisics to use Physics, we need to check Uses Physics.

place_bug_pic4

obj_wall_phisicsobj_wall_place的步事件和绘制事件如下:

The step and draw events for obj_wall_phisics and obj_wall_place are as follows:

obj_wall_phisics步事件:

obj_wall_phisicsStep Event:

if(keyboard_check(ord("W"))){phy_position_y -= 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("S"))){phy_position_y += 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("A"))){phy_position_x -= 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("D"))){phy_position_x += 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("Q"))){phy_rotation -= 3;}
if(keyboard_check(ord("E"))){phy_rotation += 3;}
if(phy_rotation > 360){
    phy_rotation -= 360;
}
if(phy_rotation < -360){
    phy_rotation += 360;
}

obj_wall_phisics绘制事件:

obj_wall_phisicsDraw Event:

draw_self();
draw_primitive_begin(pr_linestrip);
draw_vertex_color(bbox_left, bbox_top, c_black, 1);
draw_vertex_color(bbox_left, bbox_bottom, c_black, 1);
draw_vertex_color(bbox_right, bbox_bottom, c_black, 1);
draw_vertex_color(bbox_right, bbox_top, c_black, 1);
draw_vertex_color(bbox_left, bbox_top, c_black, 1);
draw_primitive_end();

obj_wall_place步事件:

obj_wall_placeStep Event:

if(keyboard_check(ord("W"))){y -= 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("S"))){y += 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("A"))){x -= 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("D"))){x += 1 / obj_mask_detection.scale;}
if(keyboard_check(ord("Q"))){image_angle += 3;}
if(keyboard_check(ord("E"))){image_angle -= 3;}
if(image_angle > 360){
    image_angle -= 360;
}
if(image_angle < -360){
    image_angle += 360;
}

obj_wall_place绘制事件:

obj_wall_placeDraw Event:

draw_self();
draw_primitive_begin(pr_linestrip);
draw_vertex_color(bbox_left, bbox_top, c_black, 1);
draw_vertex_color(bbox_left, bbox_bottom, c_black, 1);
draw_vertex_color(bbox_right, bbox_bottom, c_black, 1);
draw_vertex_color(bbox_right, bbox_top, c_black, 1);
draw_vertex_color(bbox_left, bbox_top, c_black, 1);
draw_primitive_end();

3.4. 碰撞检测 Mask Detection

physics_test_overlapplace_meeting都需要至少一个1px*1px的遮罩才能正常检测与目标的碰撞,因此我们新建一个1px*1px的spr_pixel用于检测碰撞。

physics_test_overlap and place_meeting both require at least one 1px * 1px mask to properly detect collisions with the target, so we create a new 1px * 1px spr_pixel for collision detection.

place_bug_pic3

然后新建obj_mask_detection绑定spr_pixel用于检测obj_wall_phisicsobj_wall_place的遮罩。为了使其能够使用物理,我们需要勾选Uses Physics

Then create a new obj_mask_detection binding spr_pixel to detect obj_wall_phisics and obj_wall_place masks. To enable it to use Physics, we need to check Uses Physics.

place_bug_pic5

obj_mask_detection的创建事件、步事件和绘制事件如下:

The create, step, and draw events of obj_mask_detection are as follows:

创建事件:

Create Event:

scale = 8;
depth = -1;
function check_surface(){
    if(!surface_exists(face)){
        face = surface_create(room_width * scale, room_height * scale);
    }
}
hide = 0;
face = -1;
check_surface();
surface_resize(application_surface, room_width * scale, room_height * scale);

kx = 15.5;
ky = 8;

步事件:

Step Event:

if(!hide){
    check_surface();
    surface_set_target(face);
    for(var i = 0; i < room_width * scale; i++){
        for(var j = 0; j < room_height * scale; j++){
            var overlap;
            overlap = physics_test_overlap((i + 0.5) / scale, (j + 0.5) / scale, 0, obj_wall_phisics);
            if(overlap){
                draw_point_color(i, j, #ff00ff);
                continue;
            }
            overlap = place_meeting((i + 0.5) / scale, (j + 0.5) / scale, obj_wall_place);
            if(overlap){
                draw_point_color(i, j, #ff00ff);
                continue;
            }
            draw_point_color(i, j, c_black);
        }
    }
    surface_reset_target();
}
if(keyboard_check_pressed(ord("H"))){
    hide = !hide;
}
var dy = keyboard_check(vk_down) - keyboard_check(vk_up);
var dx = keyboard_check(vk_right) - keyboard_check(vk_left);

dx /= scale;
dy /= scale;
if(dx != 0 || dy != 0){
    if(!place_meeting(kx + dx, ky + dy, obj_wall_place)
    && !physics_test_overlap(kx + dx, ky + dy, 0, obj_wall_phisics)){
        kx += dx;
        ky += dy;
    }
}

if(place_meeting(kx, ky, obj_wall_place) 
|| physics_test_overlap(kx, ky, 0, obj_wall_phisics)){
    kx = 15.5;
    ky = 8;
}

绘制事件:

Draw Event:

draw_sprite(spr_pixel, 0, kx, ky);
if(!hide){
    check_surface();
    draw_surface_stretched_ext(face, 0, 0, room_width, room_height, c_white, 0.4);
}

3.5. 房间设置 Room Setting

我们新建一个房间叫rm_test。设置他的宽高为32px * 32px,勾选Enable Physics,将重力全部设置为0,设置比例尺为1。

Let's create a new room called rm_test. Set its width and height to 32px * 32px, check Enable Physics, set all gravity to 0, and set the Pixels To Meters to 1.

place_bug_pic6

然后我们编辑背景层,设置背景颜色为灰色,方便我们进行观察。

Then we edit the background layer and set the background color to gray for easy observation.

place_bug_pic7

最后我们回到实例层,将我们所有的物体放进房间,具体如下:

Finally we go back to the instance layer and put all our objects into the room as follows:

place_bug_pic8

4. 使用方法与运行效果 Usage and diagram

运行游戏,不出意外的话会得到下图的内容:

Run the game, if nothing else, you'll get something like this:

place_bug_pic9

以下为我们实现的功能:

  1. 通过鼠标中键和滚轮,我们可以拖拽视野和缩放视野。
  2. 我们绘制了obj_wall_phisicsobj_wall_place的实际图像和函数检测到的遮罩图像,我们可以通过按下H显示或不显示遮罩,此外,我们还绘制了它们的bbox区域。
  3. 通过QE键可以修改obj_wall_phisicsobj_wall_place的角度,通过WSAD键可以修改obj_wall_phisicsobj_wall_place的坐标,我们可以观察不同角度不同坐标下obj_wall_phisicsobj_wall_place的遮罩。
  4. 我们设计了一个1px * 1px的方块,我们可以按下方向键移动它。当它在移动路径上检测到碰撞则会停下。

大家也可以尝试更换精灵图片、更改遮罩类型来测试不同情况下的检测结果。

The following are the functions we have implemented:

  1. Through the middle mouse button and scroll wheel, we can drag and zoom the field of view.
  2. We draw the actual sprite of obj_wall_phisics and obj_wall_place and the mask detected by the function, which we can show or not show the mask by pressing H.
  3. The Angle of obj_wall_phisics and obj_wall_place can be changed by the QE key, and the coordinates of obj_wall_phisics and obj_wall_place can be changed by the WSAD key. We can look at the masks of obj_wall_phisics and obj_wall_place at different angles and in different coordinates.
  4. We designed a 1px * 1px square, we can press the arrow key to move it. It stops when it detects a collision in its moving path.

You can also try to change the Sprite picture, change the mask type to test the detection results in different situations.

5. 结果与讨论 Results and discussion

现在让我们来分析碰撞结果。

Now let's analyze the results of the collision.

由于physics_test_overlapplace_meeting都需要至少一个1px * 1px的遮罩才能正常检测与目标的碰撞,而GM的像素坐标又处于像素的左上角,所以当我们从左方或上方检测遮罩时,检测到的遮罩范围会多出1px,这是正常的。因此,我们检测的碰撞结果应该如下图所示:

Because physics_test_overlap and place_meeting both require at least one 1px * 1px mask to properly detect the collision with the target, and the pixel coordinate of GM is in the upper left corner of the pixel, when we detect the mask from the left or above, The detected mask area will be 1px more, which is normal. Therefore, the collision results we detect should be as follows:

place_bug_pic10

接下来我们观察通过physics_test_overlap检测出的碰撞遮罩:

Next we observe the collision mask detected by physics_test_overlap:

place_bug_pic11

place_bug_pic12

可以看到,基于box2d的physics_test_overlap检测的遮罩形状和我们推测的基本一致,说明我们的推导过程基本无误。

It can be seen that the mask shape detected by physics_test_overlap based on box2d is basically consistent with our speculation, indicating that our deduction process is basically correct.

接下来我们观察通过place_meeting检测出的碰撞遮罩:

Next we observe the collision mask detected by place_meeting:

place_bug_pic13

place_bug_pic14

可以看到检测到的遮罩非常奇怪,甚至无法覆盖整个图像,我们用1px * 1px的方块与其碰撞,可以看到方块很明显地凹陷进了obj_wall_place

As you can see, the detected mask is very strange, it doesn't even cover the whole image, we bump it with the 1px * 1px square, and you can see that the square is clearly sunken into obj_wall_place :

place_bug_pic15

另外,当我们使用WSAD移动obj_wall_place,我们发现,遮罩的上下左右边界并不会跟跟着obj_wall_place连续地变化,而是离散地处于像素网格的中心:

In addition, when we use WSAD to move obj_wall_place, we find that the top, bottom, left and right boundaries of the mask do not change continuously with obj_wall_place, but are discretely centered in the pixel grid:

place_bug_pic16

这意味着遮罩的上下左右边界总是被限定在x.5px的位置。

This means that the top, bottom, left and right borders of the mask are always limited to the position of x.5 px.

6. 总结 Summarize

将以上结果总结如下:

  1. physics_test_overlap检测出来的碰撞遮罩基本正确。
  2. place_meeting的碰撞遮罩的边缘总是处于x.5 px的位置,这会导致物体在小数坐标时碰撞结果不正确。

place_meeting这一bug会产生不预期的行为,在写游戏时极易写出Bug,希望yoyo能认真对待并正确修复此问题。

The above results are summarized as follows:

  1. The collision mask detected by physics_test_overlap is basically correct.
  2. The edge of the collision mask of place_meeting is always at the position of x.5 px, which causes objects to collide incorrectly in decimal coordinates.

The place_meeting Bug causes unexpected behavior, it is very easy to meet bugs when programming, I hope yoyo will take this seriously and fix it properly.

Expected Change

No response

Steps To Reproduce

  1. Start GameMaker
  2. Follow the issue's Description or download the project from PlaceMeetingBug.rar
  3. MouseWheel change camera's scale; MouseButtonMiddle change camera's position; H hide mask; QE change walls' angle; WSAD change walls' position; Arrow change pixel's position
  4. The box in the left is the test of physics_test_overlap(), it is a correct collision function. The box in the right is the test of place_meetiing(), it is incorrect that you can control the blue square sink in it.
  5. The details are all in the description, Please see that!!!!!!
  6. The Contract Us Package ContractUs.zip

How reliably can you recreate this issue using your steps above?

Always

Which version of GameMaker are you reporting this issue for?

2024.400 (Betas)

Which platform(s) are you seeing the problem on?

Windows

Contact Us Package Attached?

Sample Project Added?

DragoniteSpam commented 7 months ago

Holy moly, you sure put the work into this one.

JonathanHackerCG commented 7 months ago

For some additional context, I reported this issue (or at least a subset of it) before the public GitHub system. Ticket number #209686. My request was closed, as this was determined to be intended behavior. The message I got is the following (pasted here since the ticket itself is private) emphasis added:

[...] This bug in question has been determined to be not a bug by the engineers with the following message:

"Collisions will only be flagged as happening if the bound boxes overlap and that overlap crosses a pixel centre. In this sample the calculation is at the mercy of floating point equality as the left bound is precisely on the pixel centre. If you make the offset value var OFFSET = 0.500001; then you will see that the collision is detected as it was expected.

Mathematically I wholeheartedly agree that this should be a collision, but in order to maintain some compatibility with the previous collision system this compromise had to be settled on. It also provides a better correlation to precise collisions which are tested on pixel centres." [...]

Per my follow-up request at the time, the Documentation (under Bounding Boxes) was updated to specify this:

For two instances to be in collision, their bounding boxes have to overlap. At a pixel level, an overlap is counted when the centre of that pixel is covered.

Summary: As I understand it, as of May 2023 (I reported it first in September 2022) this is considered intended behavior. However, I agree with UltraDrone and would like to see this seriously considered.