2.2 ตรีโกณและเรขาคณิต

เฟส 2 · คณิตศาสตร์สำหรับเกม · เวลาเรียน: 30–50 h

sine และ cosine สำหรับมุมและการเคลื่อนที่แบบวงกลม บวกเส้น ระนาบ ray ทรงกลม และกล่อง พร้อมการทดสอบการตัดกัน — พื้นฐานของการเล็ง การชน และการเลือกวัตถุ

ทุกอย่างในเกมมี position และส่วนใหญ่ยังมี direction ด้วย: ตัวละครหันหน้าไปทางไหน กระสุนพุ่งไปทางไหน กล้องมองไปทางไหน การแปลง "หันไปทางไหน" ให้เป็นตัวเลขคือหน้าที่ของ trigonometry (ตรีโกณมิติ — คณิตศาสตร์ว่าด้วยมุมกับวงกลม) บทนี้ยังครอบคลุมรูปทรงพื้นฐานที่เกมใช้เช็ค collision (การชนกัน) ด้วย: circle, box, ray และชุด test ที่ตอบคำถามว่า "สองสิ่งนี้แตะกันหรือเปล่า?" คุณรู้ C++ พื้นฐานและวิธีคิดกับข้อมูลอยู่แล้ว — บทนี้โค้ดจะสั้นลง คณิตศาสตร์จะเยอะขึ้น แต่รูปแบบยังเหมือนเดิม: โค้ด, output จริง, อธิบายแบบเข้าใจง่าย

ลองพิมพ์ตัวอย่างแล้วรันดูเอง บั๊กเรื่อง trig มักมองไม่เห็นจนกว่าจะเห็นตัวเลขผิดขึ้นบนจอ ดังนั้นการคุ้นเคยกับ output จริงตั้งแต่ตอนนี้จะช่วยประหยัดเวลา debug ในอนาคต

1. Radian กับ Degree: สองวิธีวัดมุม

มุมวัดได้สองแบบ: degree (องศา — วงกลมเต็มวงคือ 360) หรือ radian (เรเดียน — วงกลมเต็มวงคือ 2*pi ประมาณ 6.28318) Degree คือหน่วยที่คนทั่วไปใช้ ("หมุน 90 องศา") ส่วน radian คือหน่วยที่ math library เกือบทุกตัวใช้ภายใน รวมถึงฟังก์ชันใน <cmath> ของ C++ อย่าง sin, cos, atan2 ถ้าคุณส่ง degree เข้าไปในที่ที่มันต้องการ radian คุณจะได้คำตอบผิดโดยไม่มี error หรือ warning ใด ๆ — ฟังก์ชันยังคืนตัวเลขมาให้ปกติ แค่เป็นตัวเลขที่ผิด

degrees: 0 90 180 270 360 radians: 0 pi/2 pi 3pi/2 2pi | | | | | +-----+------+------+------+ a quarter turn = pi/2 radians = 90 degrees a full turn = 2*pi radians = 360 degrees

การแปลงหน่วยเป็นอัตราส่วนตรง ๆ: 360 องศาเท่ากับ 2*pi เรเดียน ดังนั้น 180 องศาเท่ากับ pi เรเดียน ได้สองสูตร:

radians = degrees * (PI / 180.0)
degrees = radians * (180.0 / PI)
#include <iostream>

const double PI = 3.14159265358979323846;   // C++ has no standard PI constant,
                                             // so define your own once, here
double toRadians(double degrees) { return degrees * PI / 180.0; }
double toDegrees(double radians) { return radians * 180.0 / PI; }

int main() {
    double degs[] = {0, 45, 90, 180, 270, 360};
    for (double d : degs)
        std::cout << d << " degrees = " << toRadians(d) << " radians\n";
}

Output:

0 degrees = 0 radians
45 degrees = 0.785398 radians
90 degrees = 1.5708 radians
180 degrees = 3.14159 radians
270 degrees = 4.71239 radians
360 degrees = 6.28319 radians

ดูรูปแบบ: 90 องศากลายเป็นประมาณ 1.57 ซึ่งคือ pi/2 (3.14159 / 2) 180 องศากลายเป็น 3.14159 ซึ่งคือ pi พอดี นั่นคือความสัมพันธ์ทั้งหมด — degree กับ radian เป็นแค่ไม้บรรทัดสองแบบสำหรับวัดการหมุนแบบเดียวกัน

Tip C++ ไม่มี constant PI มาตรฐานให้ใช้ตรง ๆ (บาง platform มี M_PI จาก <cmath> แต่ไม่การันตีว่าจะมีทุกที่) โปรเจกต์เกมส่วนใหญ่จะประกาศ constant ของตัวเองครั้งเดียวแบบบรรทัดข้างบน แล้วใช้ซ้ำทุกที่แทนการพิมพ์เลข pi ใหม่ทุกครั้ง

2. sin, cos, tan และ unit circle

คุณอาจเคยเรียน sine, cosine, tangent จากสามเหลี่ยมมุมฉากมาก่อน: สำหรับมุม theta (ธีตา) ในสามเหลี่ยม sin(theta) = ด้านตรงข้าม / ด้านตรงข้ามมุมฉาก, cos(theta) = ด้านประชิด / ด้านตรงข้ามมุมฉาก, และ tan(theta) = ด้านตรงข้าม / ด้านประชิด (จำง่าย ๆ ว่า SOH-CAH-TOA) นิยามนั้นถูกต้อง แต่ใช้ในเกมได้ไม่คล่องนัก เพราะโค้ดเกมแทบไม่มีสามเหลี่ยมวางอยู่ให้ใช้ — มันมีแค่มุมและอยากได้ direction

Unit circle (วงกลมหนึ่งหน่วย) คือวงกลมรัศมี 1 ที่ศูนย์กลางอยู่ที่ origin (0,0) ลองนึกภาพยืนอยู่ตรงกลางแล้วหมุนตัวไปเป็นมุม theta วัดทวนเข็มนาฬิกาจากแกน x บวก แล้วเดินออกไปถึงขอบวงกลม จุดที่คุณไปยืนอยู่จะเป็น (cos(theta), sin(theta)) เสมอ นั่นคือทริกทั้งหมด: cos ให้พิกัด x, sin ให้พิกัด y สำหรับจุดที่ห่างจากศูนย์กลางพอดี 1 หน่วย ที่มุม theta

y | | P (cos theta, sin theta) | / | / | / | / theta ----------------+--------------- x | (unit circle, radius = 1, centered at origin) theta is measured counter-clockwise from the +x axis

ทำไมมันตรงกับนิยามสามเหลี่ยม? ลากเส้นตรงจากจุด P ลงมาตั้งฉากกับแกน x จะได้สามเหลี่ยมมุมฉากที่ด้านตรงข้ามมุมฉากคือรัศมี (ยาว 1) ด้านประชิดยาว cos(theta) และด้านตรงข้ามยาว sin(theta) เพราะด้านตรงข้ามมุมฉากยาวพอดี 1 เศษส่วน "ด้านตรงข้าม/ด้านตรงข้ามมุมฉาก" กับ "ด้านประชิด/ด้านตรงข้ามมุมฉาก" จึงย่อเหลือแค่ความยาวด้านตรงข้ามกับด้านประชิดตรง ๆ คณิตเดิม แต่ภาพเข้าใจง่ายกว่า

#include <iostream>
#include <cmath>
#include <iomanip>

const double PI = 3.14159265358979323846;

int main() {
    std::cout << std::fixed << std::setprecision(3);
    double anglesDeg[] = {0, 30, 45, 60};
    for (double d : anglesDeg) {
        double rad = d * PI / 180.0;
        std::cout << "deg=" << d
                  << " cos=" << std::cos(rad)
                  << " sin=" << std::sin(rad)
                  << " tan=" << std::tan(rad) << "\n";
    }
}

Output:

deg=0 cos=1.000 sin=0.000 tan=0.000
deg=30 cos=0.866 sin=0.500 tan=0.577
deg=45 cos=0.707 sin=0.707 tan=1.000
deg=60 cos=0.500 sin=0.866 tan=1.732

ดูแถว 45 องศา: cos=0.707 และ sin=0.707 — เท่ากัน เพราะที่ 45 องศาคุณเดินมาได้ครึ่งทางพอดีระหว่าง "x ล้วน ๆ" กับ "y ล้วน ๆ" และ tan ก็คือ sin/cos ในทุกแถว (เช็คที่ 60 องศา: 0.866 / 0.500 = 1.732) — tangent เป็นแค่อัตราส่วนของอีกสองตัว ไม่ใช่ไอเดียแยกต่างหาก

Common mistake คาดหวังว่าฟังก์ชัน trig จะได้ 0 เป๊ะ ๆ ที่มุม "สวย ๆ" floating point ไม่สามารถแทน pi ได้เป๊ะ ดังนั้น cos ของมุม 90 องศาจึงไม่ได้เป็นศูนย์แบบเป๊ะ ๆ:
#include <iostream>
#include <cmath>

int main() {
    double rad = 1.5707963267948966;   // 90 degrees in radians
    double c = std::cos(rad);
    double t = std::tan(rad);
    std::cout << "cos(90) = " << c << "\n";
    std::cout << "tan(90) = " << t << "\n";
    std::cout << (c == 0.0 ? "exactly zero" : "NOT exactly zero") << "\n";
}

Output:

cos(90) = 6.12323e-17
tan(90) = 1.63312e+16
NOT exactly zero

cos(90 องศา) ในทางคณิตศาสตร์ควรเป็น 0 แต่มันพิมพ์ 6.12323e-17 ออกมา — ตัวเลขเล็กจนไม่มีความหมาย แต่ก็ไม่ใช่ bit pattern เดียวกับ 0.0 เพราะ tan = sin/cos การหารด้วยตัวเลขที่เกือบเป็นศูนย์นี้เลยระเบิดกลายเป็นตัวเลขมหาศาลไร้ความหมาย แทนที่จะเป็น "undefined" อย่างที่คุณคาดหวัง บทเรียน: อย่าเทียบผลลัพธ์ trig ด้วย == ให้เทียบกับ tolerance เล็ก ๆ แทน เช่น std::fabs(c) < 0.0001

3. เคลื่อนที่เป็นวงกลมด้วย sin และ cos

เมื่อคุณแปลงมุมให้เป็นจุดบน unit circle ได้แล้ว การทำให้อะไรบางอย่างเคลื่อนที่เป็นวงกลมก็เหลือแค่บรรทัดเดียว: scale จุดบน unit circle ด้วย radius ของวงกลม แล้วเลื่อนด้วย center ของวงกลม

x = centerX + radius * cos(angle) y = centerY + radius * sin(angle) increase "angle" a little every frame -> the point sweeps around the circle
#include <iostream>
#include <cmath>
#include <iomanip>

const double PI = 3.14159265358979323846;

int main() {
    std::cout << std::fixed << std::setprecision(3);
    double radius = 5.0;
    int steps = 5;
    for (int i = 0; i < steps; i++) {
        double angle = (2.0 * PI / steps) * i;   // spread evenly around the circle
        double x = radius * std::cos(angle);
        double y = radius * std::sin(angle);
        std::cout << "step " << i << ": angle=" << angle
                  << " pos=(" << x << ", " << y << ")\n";
    }
}

Output:

step 0: angle=0.000 pos=(5.000, 0.000)
step 1: angle=1.257 pos=(1.545, 4.755)
step 2: angle=2.513 pos=(-4.045, 2.939)
step 3: angle=3.770 pos=(-4.045, -2.939)
step 4: angle=5.027 pos=(1.545, -4.755)

ห้า step กระจายเท่า ๆ กันรอบวงกลมรัศมี 5 ที่ศูนย์กลางอยู่ที่ origin สังเกตว่าจุดต่าง ๆ เรียงเป็นรูปดาวห้าแฉกรอบ (0,0) แต่ละจุดห่างจากศูนย์กลางพอดี 5 หน่วย (เช็คได้: 1.545^2 + 4.755^2 ออกมาประมาณ 25 ซึ่งคือ radius^2) ในเกมจริง "angle" จะค่อย ๆ โตขึ้นทุกเฟรม (angle += turnSpeed * deltaTime) แทนที่จะกระโดดทีละ 5 step แบบตายตัว — นี่คือวิธี animate ดาวเทียมที่โคจร, เส้นเรดาร์ที่กวาด, หรือเข็มนาฬิกา

Tip สูตรเดียวกันนี้ยังใช้วางของให้กระจายเท่า ๆ กันรอบวงกลมได้โดยไม่ต้องมีการเคลื่อนไหวเลย — จุด spawn รอบสนามรบ, ไอคอนรอบเมนูวงกลม, ศัตรูล้อมรอบบอส แค่คำนวณ angle = (2*PI / count) * i สำหรับแต่ละไอเทม i คุณก็จะได้วงแหวนที่กระจายเท่า ๆ กันแบบฟรี ๆ

4. หมุน 2D vector ด้วยมุม

หัวข้อ 3 หมุน จุดรอบ origin โดยใช้มุมที่เปลี่ยนไปเรื่อย ๆ ทีนี้มาดูเครื่องมือที่ทั่วไปกว่านั้น: กำหนด 2D vector ใดก็ได้ (x, y) กับมุมหนึ่ง แล้วสร้าง vector เดิมที่หมุนไปตามมุมนั้น เรียกว่า rotation (การหมุน) และสูตรคือ:

new_x = x * cos(angle) - y * sin(angle) new_y = x * sin(angle) + y * cos(angle)

คุณไม่จำเป็นต้อง derive สูตรนี้เพื่อใช้มัน — ใช้มันเป็นสูตรสำเร็จ เหมือนที่คุณใช้ sqrt เป็นสูตรสำเร็จ มันเอา vector มาหมุนรอบ (0,0) ไปเป็นมุม angle ทวนเข็มนาฬิกาถ้ามุมเป็นบวก

#include <iostream>
#include <cmath>
#include <iomanip>

const double PI = 3.14159265358979323846;

struct Vec2 { double x, y; };

Vec2 rotate(Vec2 v, double angleRad) {
    double c = std::cos(angleRad);
    double s = std::sin(angleRad);
    return { v.x * c - v.y * s,    // new x
             v.x * s + v.y * c };  // new y
}

int main() {
    std::cout << std::fixed << std::setprecision(3);
    Vec2 v = {2.0, 0.0};
    double anglesDeg[] = {0, 90, 180, 270};
    for (double deg : anglesDeg) {
        Vec2 r = rotate(v, deg * PI / 180.0);
        std::cout << deg << " deg: (" << r.x << ", " << r.y << ")\n";
    }
}

Output:

0 deg: (2.000, 0.000)
90 deg: (0.000, 2.000)
180 deg: (-2.000, 0.000)
270 deg: (0.000, -2.000)

เริ่มจาก vector ที่ชี้ไปทางขวา (2, 0) หมุน 90 องศา มันจะชี้ขึ้นตรง ๆ (0, 2) หมุนอีก 90 มันชี้ไปทางซ้าย (-2, 0) หมุนอีก 90 มันชี้ลง (0, -2) มันเดินรอบวงกลมเต็มวงด้วยการหมุนทีละ 90 องศาสี่ครั้ง โดยความยาวคงเดิมตลอด (2 หน่วย)

(0,2) | | (-2,0) -----+----- (2,0) start here, angle = 0 | | (0,-2) rotating by +90 each time walks: right -> up -> left -> down
Common mistake สมมติเอาเองว่า "มุมบวก = หมุนตามเข็ม" หรือกลับกัน โดยไม่เช็ค coordinate system ที่ใช้อยู่ สูตรข้างบนหมุนทวนเข็มนาฬิกาเมื่อแกน y ชี้ขึ้น (แบบที่คณิตศาสตร์ใช้ และแบบที่ 2D world space ของ Unity ใช้) แต่ screen-space พิกัดพิกเซลมักให้แกน y ชี้ ลง — ในระบบนั้นสูตรเดียวกันเป๊ะ ๆ จะหมุนตามเข็มนาฬิกาแทน ไม่มีอันไหนผิด แค่ต้องรู้ว่ากำลังวาดในระบบพิกัดแบบไหน

5. เล็งไปที่เป้าหมาย: atan2 กับ direction vector

หัวข้อ 3 กับ 4 เริ่มจากมุมแล้วไปหา position หรือ vector ที่หมุนแล้ว การเล็ง (aiming) เป็นปัญหากลับกัน: คุณมีสอง position อยู่แล้ว — ตัวยิงกับเป้าหมาย — และอยากได้ มุม ระหว่างสองจุดนั้น เครื่องมือสำหรับงานนี้คือ atan2(dy, dx) ซึ่งรับผลต่างของ y กับผลต่างของ x แล้วคืนมุมของ vector (dx, dy) วัดแบบเดียวกับ unit circle ในหัวข้อ 2

y | | * target (tx, ty) | / | / | / angle = atan2(dy, dx) | / *---------+----------------- x shooter (px,py) dx = tx-px, dy = ty-py
#include <iostream>
#include <cmath>
#include <iomanip>

const double PI = 3.14159265358979323846;
double toDegrees(double rad) { return rad * 180.0 / PI; }

int main() {
    std::cout << std::fixed << std::setprecision(2);
    struct { double dx, dy; } targets[] = {
        {5, 0}, {0, 5}, {-5, 0}, {0, -5}, {3, 3}
    };
    for (auto t : targets) {
        double angle = std::atan2(t.dy, t.dx);
        std::cout << "dx=" << t.dx << " dy=" << t.dy
                  << " -> angle=" << toDegrees(angle) << " deg\n";
    }
}

Output:

dx=5.00 dy=0.00 -> angle=0.00 deg
dx=0.00 dy=5.00 -> angle=90.00 deg
dx=-5.00 dy=0.00 -> angle=180.00 deg
dx=0.00 dy=-5.00 -> angle=-90.00 deg
dx=3.00 dy=3.00 -> angle=45.00 deg

ทุกผลลัพธ์ตรงกับภาพ unit circle: เป้าหมายที่อยู่ขวาตรง ๆ คือ 0 องศา, บนตรง ๆ คือ 90, ซ้ายตรง ๆ คือ 180, ล่างตรง ๆ คือ -90 (ติดลบเพราะหมุนไปอีกทาง) และแนวทแยงบนขวาคือ 45

Common mistake คำนวณมุมด้วย atan(dy / dx) แทนที่จะเป็น atan2(dy, dx) มีปัญหาสองอย่าง: การหารด้วย dx = 0 (เป้าหมายอยู่ตรงบนหรือล่างพอดี) จะพัง หรือได้ infinity และ atan ธรรมดาไม่สามารถแยกเป้าหมายที่อยู่ข้างหน้ากับข้างหลังได้ — atan(1/1) กับ atan(-1/-1) ได้มุมเท่ากัน ทั้งที่สองเป้าหมายอยู่คนละทิศตรงข้ามกันเป๊ะ atan2 ดูเครื่องหมายของ ทั้ง dx และ dy แยกกัน จึงคืนมุมที่ถูกต้องครบทุกทิศเสมอ

พอได้มุมมาแล้ว ทริกจากหัวข้อ 2 จะแปลงมันกลับเป็น direction: (cos(angle), sin(angle)) คือ unit vector (ความยาวเท่ากับ 1 พอดี) ที่ชี้ตรงไปที่เป้าหมาย เอาไป scale ด้วยความเร็ว คุณก็จะได้ velocity ที่พุ่งเข้าหาเป้าหมาย — นี่คือวิธีที่กระสุนโฮมมิ่งง่าย ๆ หรือการหมุน "มองไปที่ผู้เล่น" ทำงาน

#include <iostream>
#include <cmath>
#include <iomanip>

int main() {
    std::cout << std::fixed << std::setprecision(2);

    double px = 0, py = 0;      // bullet start position
    double tx = 8, ty = 6;      // target position (does not move)
    double speed = 5.0;         // units per step

    double dx = tx - px;
    double dy = ty - py;
    double angle = std::atan2(dy, dx);     // angle from bullet to target

    double vx = std::cos(angle) * speed;   // velocity x
    double vy = std::sin(angle) * speed;   // velocity y

    std::cout << "aim angle = " << angle << " rad\n";
    for (int step = 1; step <= 3; step++) {
        px += vx;
        py += vy;
        std::cout << "step " << step << ": pos=(" << px << ", " << py << ")\n";
    }
}

Output:

aim angle = 0.64 rad
step 1: pos=(4.00, 3.00)
step 2: pos=(8.00, 6.00)
step 3: pos=(12.00, 9.00)

เป้าหมายอยู่ที่ (8, 6) ซึ่งบังเอิญห่างออกไปพอดี 10 หน่วย (สามเหลี่ยมมุมฉาก 6-8-10 เหมือนสามเหลี่ยม 3-4-5 ที่คุ้นเคยแต่คูณสอง) ที่ความเร็ว 5 กระสุนจะเดินทางระยะนั้นได้พอดีใน 2 step ลงจอดตรง (8, 6) เป๊ะ — แล้วก็เดินทางเลยเป้าหมายไปต่อใน step 3 เพราะไม่มีอะไรบอกให้มันหยุด กระสุนโฮมมิ่งจริง ๆ จะคำนวณ angle ใหม่ทุกเฟรมไปยัง position ปัจจุบัน ของเป้าหมาย และเช็คระยะที่เหลือเพื่อให้มันหยุดหรือระเบิดตอนถึงเป้า

6. Line, Ray, และ Plane

ก่อนจะไปถึง collision test คุณต้องรู้จักรูปทรงที่ test เหล่านั้นทำงานด้วยก่อน มีรูปทรง "อนันต์" สามแบบที่เจอบ่อยมาก:

LINE (both directions go on forever): <----------P---D----------> RAY (starts at a point, one direction goes on forever): origin *----------D---------> PLANE (a flat surface, described by a point and a normal): --------------------------------- <- the plane ^ | n (normal: perpendicular to the surface)

Ray เขียนเป็นสูตรได้: เลือกตัวเลข t >= 0 อะไรก็ได้ จุดที่ "ระยะ" t ตามแนว ray คือ origin + t * direction t ยิ่งมากยิ่งไกลตามแนว ray t ตัวนี้จะโผล่มาอีกในทุก intersection test ต่อจากนี้ในบทนี้ — มันคือคำตอบของ "เราชนอะไรตรงระยะไหนตามแนว ray"

#include <iostream>

struct Vec2 { double x, y; };

Vec2 pointOnRay(Vec2 origin, Vec2 dir, double t) {
    return { origin.x + t * dir.x, origin.y + t * dir.y };
}

int main() {
    Vec2 origin = {0, 0};
    Vec2 dir    = {1, 0.5};    // the direction the ray travels

    for (double t = 0; t <= 3; t += 1.0) {
        Vec2 p = pointOnRay(origin, dir, t);
        std::cout << "t=" << t << " -> point=(" << p.x << ", " << p.y << ")\n";
    }
}

Output:

t=0 -> point=(0, 0)
t=1 -> point=(1, 0.5)
t=2 -> point=(2, 1)
t=3 -> point=(3, 1.5)
Tip ที่นี่ dir มีความยาว sqrt(1^2 + 0.5^2) ~= 1.118 ไม่ใช่ 1 พอดี นั่นแปลว่า t ไม่เท่ากับระยะทางจริงที่เดินไป — ที่ t=1 จุดเคลื่อนไป 1.118 หน่วย ไม่ใช่ 1 สูตร intersection เกือบทุกอันในบทนี้สมมติว่า dir เป็น unit vector (ความยาวเท่ากับ 1 พอดี) เพื่อให้ t เท่ากับระยะทางตรง ๆ ให้ normalize direction ของ ray (หารมันด้วยความยาวของตัวมันเอง) ก่อนส่งเข้า collision test เสมอ

ทีนี้มาถึง dot product — เครื่องมือเล็ก ๆ ที่คุณจะใช้ซ้ำ ๆ ต่อจากนี้ สำหรับ vector สองตัว a กับ b, dot(a, b) = a.x*b.x + a.y*b.y มันเป็นตัวเลขตัวเดียว ไม่ใช่ vector และเครื่องหมายของมันบอกว่า vector ทั้งสองชี้ไปทางไหนเทียบกัน

#include <iostream>

struct Vec2 { double x, y; };

double dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }

int main() {
    Vec2 right     = {1, 0};
    Vec2 up        = {0, 1};
    Vec2 alsoRight = {5, 0};
    Vec2 leftish   = {-1, 0.2};

    std::cout << "right . up = "        << dot(right, up)        << "\n";
    std::cout << "right . alsoRight = " << dot(right, alsoRight) << "\n";
    std::cout << "right . leftish = "   << dot(right, leftish)   << "\n";
}

Output:

right . up = 0
right . alsoRight = 5
right . leftish = -1

right กับ up ตั้งฉากกัน (ห่างกัน 90 องศา) และ dot product ของมันได้ 0 เป๊ะ — นี่คือกฎทั่วไป: dot product เป็น 0 เมื่อ vector สองตัวตั้งฉากกัน right กับ alsoRight ชี้ไปทางเดียวกัน dot product เลยออกมาเป็นบวก right กับ leftish ชี้ไปคนละทางเกือบตรงข้าม เลยออกมาเป็นลบ บวกแปลว่า "ชี้ไปทางเดียวกันโดยรวม", ลบแปลว่า "ชี้ไปทางตรงข้ามโดยรวม", ศูนย์แปลว่า "ตั้งฉากกัน"

ทริกเรื่องเครื่องหมายนี้แหละคือวิธีที่ signed distance (ระยะทางแบบมีเครื่องหมาย) ของ plane ทำงาน: สำหรับ plane ที่มี normal n ผ่านจุด Q signed distance ของจุด P จาก plane คือ dot(n, P - Q) ศูนย์แปลว่า P อยู่บน plane, บวกแปลว่า P อยู่ฝั่งที่ normal ชี้ไป, ลบแปลว่าอยู่อีกฝั่ง

#include <iostream>

struct Vec2 { double x, y; };
double dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }

int main() {
    Vec2 pointOnPlane = {0, 0};   // the ground, at y=0
    Vec2 normal       = {0, 1};   // "up" is the positive side

    Vec2 above = {3, 5};
    Vec2 below = {3, -2};

    Vec2 toAbove = { above.x - pointOnPlane.x, above.y - pointOnPlane.y };
    Vec2 toBelow = { below.x - pointOnPlane.x, below.y - pointOnPlane.y };

    std::cout << "above: signed dist = " << dot(normal, toAbove) << "\n";
    std::cout << "below: signed dist = " << dot(normal, toBelow) << "\n";
}

Output:

above: signed dist = 5
below: signed dist = -2

จุด (3, 5) อยู่เหนือ ground plane ขึ้นไป 5 หน่วย และสูตรรายงาน +5 จุด (3, -2) อยู่ต่ำกว่า 2 หน่วย และมันรายงาน -2 ใน 3D สูตรเดียวกันเป๊ะ ๆ (ด้วย normal และจุดแบบ 3D) คือวิธีที่ engine เช็คว่าอะไรบางอย่างอยู่ฝั่งไหนของพื้น, กำแพง, หรือหน้าของ camera frustum

7. Circle, Sphere, และ Axis-Aligned Box (AABB)

การเช็คว่า mesh ตัวละครที่มีรายละเอียดสองตัวทับกันหรือเปล่า ทีละ triangle นั้นแพงมาก เกมแทบไม่เคยทำแบบนั้นตรง ๆ แต่จะห่อแต่ละ object ด้วย bounding shape (รูปทรงล้อมรอบ) ง่าย ๆ แล้วเช็คตัวนั้นแทน — ถูก, ประมาณ, และดีพอสำหรับ gameplay ส่วนใหญ่ bounding shape ที่ง่ายที่สุดสองแบบคือ circle (เรียก sphere ใน 3D) กับ box

Circle ต้องการแค่ center กับ radius ใน 3D ไอเดียเดียวกันเรียกว่า sphere AABB (axis-aligned bounding box) คือสี่เหลี่ยม (หรือใน 3D คือ box) ที่ขอบขนานกับแกน x และ y เสมอ — ไม่มีการหมุนเลย ข้อจำกัดนี้แหละที่ทำให้มันถูก: คุณต้องเก็บแค่สองมุม คือมุมค่าน้อยสุดกับมุมค่ามากสุด

CIRCLE: AABB (axis-aligned box): (minX,minY) (maxX,minY) * * * *--------------------* * * | | * center * | | * (cx,cy) * radius | | * * *--------------------* * * * (minX,maxY) (maxX,maxY)
#include <iostream>

struct Vec2 { double x, y; };

struct Circle {
    Vec2 center;
    double radius;
};

struct AABB {              // Axis-Aligned Bounding Box
    Vec2 min;                // corner with the smallest x and y
    Vec2 max;                // corner with the largest x and y
};

int main() {
    Circle c = { {10.0, 10.0}, 3.0 };
    AABB box = { {0.0, 0.0}, {4.0, 2.0} };

    std::cout << "circle: center=(" << c.center.x << "," << c.center.y
              << ") radius=" << c.radius << "\n";

    double width  = box.max.x - box.min.x;
    double height = box.max.y - box.min.y;
    std::cout << "box: width=" << width << " height=" << height << "\n";
}

Output:

circle: center=(10,10) radius=3
box: width=4 height=2

ไม่มีอะไรซับซ้อน: circle เก็บตัวเลขสามตัว, box เก็บสี่ตัว (สองมุม) และทั้งคู่ให้คุณคำนวณค่าอื่นที่มีประโยชน์ (width, height, diameter) ได้ด้วยการลบง่าย ๆ

Tip เกมมักรัน bounding-shape check ที่ถูก ๆ ก่อน (circle vs circle หรือ AABB vs AABB) เพื่อตัดคู่ object ที่เห็นชัดว่าไม่มีทางชนกันออกไปเร็ว ๆ แล้วค่อยรัน check ที่แพงและแม่นยำเฉพาะกับคู่ที่เหลือรอด รูปแบบสองขั้นตอนนี้เรียกว่า broad phase ตามด้วย narrow phase collision detection และนี่คือเหตุผลที่ physics engine ทุกตัวพึ่งพารูปทรงง่าย ๆ ในหัวข้อนี้อย่างหนัก

8. Point-in-circle และ point-in-rectangle test

คำถาม collision ที่ง่ายที่สุดคือ "จุดนี้อยู่ในรูปทรงนี้ไหม?" — ใช้กับการคลิกเมาส์, spawn check, และ trigger zone สำหรับ circle จุดจะอยู่ข้างในเมื่อระยะห่างจาก center น้อยกว่าหรือเท่ากับ radius

* * * * * * C * * (cx,cy) * * r * * P_in(o) * P_in: distance(P_in, C) <= r -> INSIDE * * * * * P_out(o) distance(P_out, C) > r -> OUTSIDE
#include <iostream>

struct Vec2 { double x, y; };

bool pointInCircle(Vec2 p, Vec2 center, double radius) {
    double dx = p.x - center.x;
    double dy = p.y - center.y;
    double distSq = dx*dx + dy*dy;         // squared distance -- no sqrt needed
    return distSq <= radius * radius;
}

int main() {
    Vec2 center = {0, 0};
    double radius = 5.0;

    Vec2 inside  = {3, 4};   // distance = 5 exactly (a 3-4-5 triangle)
    Vec2 outside = {5, 5};   // distance = sqrt(50) ~= 7.07

    std::cout << "inside point in circle? "  << pointInCircle(inside, center, radius)  << "\n";
    std::cout << "outside point in circle? " << pointInCircle(outside, center, radius) << "\n";
}

Output:

inside point in circle? 1
outside point in circle? 0

(3, 4) ห่างจาก origin พอดี 5 หน่วย (สามเหลี่ยมมุมฉาก 3-4-5) ซึ่งเท่ากับ radius พอดี — เส้นขอบนับเป็น "อยู่ข้างใน" ในที่นี้เพราะ test ใช้ <= (5, 5) ห่างประมาณ 7.07 หน่วย ไกลกว่า radius เลยไม่ผ่าน std::cout พิมพ์ bool เป็น 1 สำหรับ true และ 0 สำหรับ false

Tip โค้ดเทียบ distSq <= radius*radius แทนที่จะเป็น sqrt(distSq) <= radius ทั้งสองแบบให้คำตอบ true/false เหมือนกัน เพราะการยกกำลังสองรักษาลำดับไว้สำหรับตัวเลขที่ไม่ติดลบ แต่แบบยกกำลังสองข้าม sqrt ไปได้เลย ดูเหมือนประหยัดนิดเดียว แต่เมื่อคุณเช็ค object เป็นร้อยตัวทุกเฟรม การข้าม square root ที่ไม่จำเป็นเป็นพัน ๆ ครั้งรวมกันแล้วมีผล ให้ใช้แบบ squared-distance เป็นนิสัย

Point-in-rectangle (AABB) test ยิ่งง่ายกว่าอีก: เช็คว่า x ของจุดอยู่ระหว่าง min กับ max x ของ box แล้วแยกเช็ค y ว่าอยู่ระหว่าง min กับ max y

(minX,minY) *------------------* (maxX,minY) | | | P_in | | | (minX,maxY) *------------------* (maxX,maxY) P_out (x > maxX -> outside)
#include <iostream>

struct Vec2 { double x, y; };
struct AABB { Vec2 min, max; };

bool pointInAABB(Vec2 p, AABB box) {
    return p.x >= box.min.x && p.x <= box.max.x
        && p.y >= box.min.y && p.y <= box.max.y;
}

int main() {
    AABB box = { {0, 0}, {10, 5} };

    Vec2 inside  = {4, 3};
    Vec2 outside = {12, 3};

    std::cout << "inside point in box? "  << pointInAABB(inside, box)  << "\n";
    std::cout << "outside point in box? " << pointInAABB(outside, box) << "\n";
}

Output:

inside point in box? 1
outside point in box? 0

สี่การเปรียบเทียบ เอามา "and" กันด้วย && ถ้าอันไหนอันหนึ่งไม่ผ่าน จุดนั้นก็อยู่ข้างนอก นี่คือ intersection test ที่ถูกที่สุดในบทนี้ทั้งบท และยังเป็น building block หลักที่ AABB-vs-AABB collision เอาไปใช้ซ้ำในแบบพี่น้องกัน (box overlap test ก็แค่ทำ min/max comparison แบบเดียวกันนี้กับ box ทั้งสองแทนที่จะเป็น box กับจุด)

9. ระยะห่างจากจุดถึงเส้นตรง

บางครั้งคุณไม่ได้ต้องการ "อยู่ข้างในหรือข้างนอก" แค่ต้องการ "จุดนี้ห่างจากเส้นนั้นแค่ไหน" — มีประโยชน์กับเรื่องอย่างการรักษาระยะห่างขั้นต่ำของ object จากกำแพง หรือเช็คว่ากระสุนที่ยิงพลาดผ่านเป้าหมายไปใกล้แค่ไหน เรื่องนี้ต้องการเครื่องมือเล็ก ๆ เพิ่มอีกอัน: 2D cross product ต่างจาก cross product แบบ 3D (ที่คืนค่าเป็น vector) เวอร์ชัน 2D คืนตัวเลขตัวเดียว: cross(a, b) = a.x*b.y - a.y*b.x ขนาดของมันเกี่ยวข้องกับพื้นที่ของ parallelogram ที่ vector สองตัวนั้นจะสร้างขึ้น และเครื่องหมายของมันบอกว่า b อยู่ฝั่งไหนเทียบกับ a

P | | d (perpendicular distance) | A ----------------+----------------- B foot of the perpendicular
#include <iostream>
#include <cmath>

struct Vec2 { double x, y; };

double distancePointToLine(Vec2 p, Vec2 a, Vec2 b) {
    Vec2 d  = { b.x - a.x, b.y - a.y };      // line direction (A to B)
    Vec2 ap = { p.x - a.x, p.y - a.y };      // vector from A to P
    double cross = d.x * ap.y - d.y * ap.x;  // 2D cross product (a single number)
    double lenD = std::sqrt(d.x*d.x + d.y*d.y);
    return std::fabs(cross) / lenD;
}

int main() {
    Vec2 a = {0, 0};
    Vec2 b = {10, 0};       // a horizontal line along the x-axis
    Vec2 p = {4, 3};

    std::cout << "distance = " << distancePointToLine(p, a, b) << "\n";
}

Output:

distance = 3

คุณเช็คด้วยตาได้เลยว่าคำตอบนี้ถูก: เส้นวิ่งตามแนว y = 0 และจุดอยู่ที่ y = 3 ดังนั้นระยะตั้งฉากก็ชัดเจนว่าเป็น 3 สูตรก็ให้คำตอบตรงกัน: cross = 10*3 - 0*4 = 30, ความยาวของเส้น lenD = 10, และ 30 / 10 = 3 cross product วัดพื้นที่ของ parallelogram ที่เกิดจาก direction ของเส้นกับ vector ไปยังจุดนั้น การหารด้วยความยาวของเส้นจะแปลงพื้นที่นั้นให้กลายเป็นระยะตั้งฉากธรรมดา

Tip เอา std::fabs ออก แล้ว เครื่องหมาย ของ cross product จะบอกว่าจุดนั้นอยู่ฝั่งไหนของเส้น — บวกฝั่งหนึ่ง ลบอีกฝั่งหนึ่ง ไอเดียเดียวนี้ (เครื่องหมายของ 2D cross product) คือวิธีที่ triangle rasterizer ใช้ตัดสินว่า pixel ไหนอยู่ในสามเหลี่ยม และวิธีที่โค้ด collision 2D ง่าย ๆ ใช้ตัดสินว่าคุณยืนอยู่ฝั่งไหนของกำแพง

10. Ray vs sphere (circle) intersection

Test นี้ตอบคำถาม "ray นี้ชน circle นี้ไหม แล้วชนที่ระยะเท่าไหร่ตามแนว ray?" — คำถามที่อาวุธแบบ hitscan, ray สำหรับ mouse-picking, หรือการเช็ค line-of-sight ต้องการคำตอบพอดี ไอเดียทางเรขาคณิต: หาจุดบนเส้น ray ที่ใกล้ center ของ circle ที่สุด แล้วดูว่าจุดที่ใกล้ที่สุดนั้นอยู่ในระยะ radius หรือเปล่า

* * * * * * C * O ----------tca----* (center) * (origin) ------dir---> * * * * t0 * t1 * * * tca = how far along the ray the closest approach to C is t0, t1 = the two points where the ray line crosses the circle
#include <iostream>
#include <cmath>

struct Vec2 { double x, y; };

double dot(Vec2 a, Vec2 b) { return a.x*b.x + a.y*b.y; }

bool raySphere(Vec2 origin, Vec2 dir, Vec2 center, double radius, double& tHit) {
    Vec2 L = { center.x - origin.x, center.y - origin.y };
    double tca = dot(L, dir);              // closest-approach distance along the ray
    double d2 = dot(L, L) - tca * tca;     // squared distance from center to the ray line
    double r2 = radius * radius;
    if (d2 > r2) return false;             // ray line misses the circle entirely

    double thc = std::sqrt(r2 - d2);       // half-chord length
    double t0 = tca - thc;                 // near hit
    double t1 = tca + thc;                 // far hit

    if (t0 >= 0) { tHit = t0; return true; }
    if (t1 >= 0) { tHit = t1; return true; }
    return false;                          // circle is entirely behind the ray's origin
}

int main() {
    Vec2 origin = {0, 0};
    Vec2 center = {10, 0};
    double radius = 2.0;

    Vec2 dir1 = {1, 0};    // points straight at the circle, unit length
    Vec2 dir2 = {0, 1};    // points straight up, away from the circle

    double t;
    if (raySphere(origin, dir1, center, radius, t))
        std::cout << "dir1: hit at t=" << t << "\n";
    else
        std::cout << "dir1: no hit\n";

    if (raySphere(origin, dir2, center, radius, t))
        std::cout << "dir2: hit at t=" << t << "\n";
    else
        std::cout << "dir2: no hit\n";
}

Output:

dir1: hit at t=8
dir2: no hit

Circle มี center อยู่ที่ (10, 0) radius 2 ดังนั้นมันครอบคลุมตั้งแต่ x=8 ถึง x=12 ตามแนวแกน x ray แรกยิงตรงตามแนว +x จาก origin และตามที่คาดไว้ มันแตะ circle ครั้งแรกที่ x=8 ซึ่งคือ t=8 step ตามแนว direction ที่ยาว 1 หน่วย ray ที่สองยิงขึ้นตรง ๆ — มันไม่มีทางเข้าใกล้ x=10 เลย จึงพลาดไม่ว่าจะเดินทางไกลแค่ไหน และฟังก์ชันรายงานว่าไม่ชนได้ถูกต้อง

Common mistake ส่ง dir ที่ไม่ใช่ unit vector เข้าไป ถ้า dir มีความยาว 2 ทุก "ระยะทาง" ที่สูตรคำนวณจะคลาดเคลื่อนไปด้วยตัวคูณเดียวกันนั้น และ tHit ก็เลิกมีความหมาย "ระยะถึงจุดที่ชน" — มันยังบอกได้ว่ามีการชนเกิดขึ้น แต่ตัวเลขที่รายงานผิด normalize direction ของ ray ก่อนเรียก test แบบนี้เสมอ

11. Ray vs AABB: the slab test

Test สุดท้าย: ray ชน axis-aligned box หรือเปล่า? วิธีคลาสสิกเรียกว่า slab test ลองนึกภาพช่วง x ของ box เป็น "slab" (แผ่น) — แถบยาวไม่มีที่สิ้นสุดระหว่าง minX กับ maxX คำนวณช่วงของค่า t ที่ ray อยู่ในแถบนั้น ทำแบบเดียวกันกับช่วง y ray จะชน box จริง ๆ ก็ต่อเมื่อช่วงทั้งสองทับซ้อนกัน

y | 5 --+----*-------*---- <- box top (maxY) | | BOX | 1 --+----*-------*---- <- box bottom (minY) | 4 8 +----+-------+-------- x (minX) (maxX) ray from (0,3) heading +x: enters the x-slab at t=4, and y=3 is inside the y-range the whole time -> HIT at t=4
#include <iostream>
#include <algorithm>
#include <limits>

struct Vec2 { double x, y; };
struct AABB { Vec2 min, max; };

bool raySlabAABB(Vec2 origin, Vec2 dir, AABB box, double& tHit) {
    double tMin = -std::numeric_limits<double>::infinity();
    double tMax =  std::numeric_limits<double>::infinity();

    // X axis slab
    if (dir.x != 0.0) {
        double t1 = (box.min.x - origin.x) / dir.x;
        double t2 = (box.max.x - origin.x) / dir.x;
        if (t1 > t2) std::swap(t1, t2);
        tMin = std::max(tMin, t1);
        tMax = std::min(tMax, t2);
    } else if (origin.x < box.min.x || origin.x > box.max.x) {
        return false;   // parallel to the x-slab and outside it -- can never hit
    }

    // Y axis slab
    if (dir.y != 0.0) {
        double t1 = (box.min.y - origin.y) / dir.y;
        double t2 = (box.max.y - origin.y) / dir.y;
        if (t1 > t2) std::swap(t1, t2);
        tMin = std::max(tMin, t1);
        tMax = std::min(tMax, t2);
    } else if (origin.y < box.min.y || origin.y > box.max.y) {
        return false;
    }

    if (tMin > tMax || tMax < 0) return false;  // slabs don't overlap, or box is behind us
    tHit = tMin;
    return true;
}

int main() {
    AABB box = { {4, 1}, {8, 5} };

    Vec2 origin1 = {0, 3};
    Vec2 dir1    = {1, 0};    // straight along +x, at y=3 (inside the box's y-range)

    Vec2 origin2 = {0, 3};
    Vec2 dir2    = {0, 1};    // straight up -- x never reaches the box

    double t;
    if (raySlabAABB(origin1, dir1, box, t))
        std::cout << "ray1: hit at t=" << t << "\n";
    else
        std::cout << "ray1: no hit\n";

    if (raySlabAABB(origin2, dir2, box, t))
        std::cout << "ray2: hit at t=" << t << "\n";
    else
        std::cout << "ray2: no hit\n";
}

Output:

ray1: hit at t=4
ray2: no hit

Ray 1 เริ่มที่ (0, 3) แล้วเดินตามแนว +x ค่า y ของมันไม่เปลี่ยนเลย และ 3 ก็อยู่ในช่วง y ของ box [1, 5] อยู่แล้ว ดังนั้น y-slab จึงไม่เพิ่มข้อจำกัดอะไร การเช็ค x-slab ของมันพบว่ามันเข้าสู่ช่วง x ของ box [4, 8] ที่ t=4 — นั่นกลายเป็นคำตอบ Ray 2 เดินขึ้นตรง ๆ จากจุดเริ่มเดียวกัน ค่า x ของมันเป็น 0 ตลอดไป ซึ่งอยู่นอกช่วง x ของ box [4, 8] ไม่ว่าจะเดินทางไกลแค่ไหน ดังนั้นการเช็คแรกสุด (dir.x == 0 และ origin.x อยู่นอก slab) ก็ปฏิเสธมันทันที

Common mistake หารด้วย dir.x หรือ dir.y โดยไม่เช็คว่าเป็นศูนย์ก่อน ray ที่เดินทางแนวนอนล้วน ๆ จะมี dir.y == 0 และการหารด้วยศูนย์เป็น undefined behavior สำหรับ integer และให้ inf/nan สำหรับ floating point — ไม่ว่าแบบไหนมันก็ทำให้การคำนวณที่เหลือเสียหายแบบเงียบ ๆ โค้ดข้างบนจัดการแกนนั้นเป็นกรณีพิเศษ: ถ้า ray ไม่เคลื่อนที่ตามแกนนั้นเลย มันจะอยู่ใน slab นั้นตลอด (origin อยู่ระหว่าง min กับ max อยู่แล้ว) หรือไม่อยู่เลย และไม่ต้องหารเลยไม่ว่ากรณีไหน

12. Glossary

13. Exercises

Exercise 1 (a) แปลง 135 องศาเป็น radian (b) ใช้สูตร rotate() จากหัวข้อ 4 คำนวณผลลัพธ์ของการหมุน vector (1, 0) ไป 135 องศา ปัดเศษให้เหลือ 3 ตำแหน่งทศนิยม คุณใช้ค่าที่รู้อยู่แล้วได้: cos(135deg) = -0.707107, sin(135deg) = 0.707107
Show answer

(a) radians = degrees * PI / 180 = 135 * 3.14159265 / 180 ~= 2.356 radian (นี่คือ 3*PI/4)

(b) ใช้ new_x = x*cos(a) - y*sin(a) และ new_y = x*sin(a) + y*cos(a) โดย x=1, y=0:

new_x = 1 * (-0.707107) - 0 * 0.707107 = -0.707
new_y = 1 * 0.707107 + 0 * (-0.707107) = 0.707

ดังนั้น (1, 0) หมุนไป 135 องศา ได้ประมาณ (-0.707, 0.707) — ชี้ขึ้นและไปทางซ้าย ซึ่งตรงกับสามัญสำนึก: 135 องศาผ่านจุดชี้ขึ้นตรง ๆ (90) ไปแล้วและมาได้ครึ่งทางสู่จุดชี้ซ้ายตรง ๆ (180)

Exercise 2 Circle มี center (2, 3) radius 4 สี่เหลี่ยม (AABB) มีมุม min=(0, 0) และ max=(5, 5) สำหรับจุด (5, 5): มันอยู่ใน circle ไหม? อยู่ในสี่เหลี่ยมไหม? แสดงวิธีทำโดยใช้สูตรจากหัวข้อ 8
Show answer

Circle test: dx = 5 - 2 = 3, dy = 5 - 3 = 2, distSq = 3*3 + 2*2 = 9 + 4 = 13 radius*radius = 4*4 = 16 เพราะ 13 <= 16 จุดนี้ อยู่ข้างใน circle

AABB test: 5 >= 0 && 5 <= 5 ไหม? ใช่ (5 อยู่บนขอบพอดี ซึ่งนับด้วย) แล้ว 5 >= 0 && 5 <= 5 สำหรับ y ล่ะ? ใช่เหมือนกัน ดังนั้นจุดนี้ อยู่ข้างใน AABB ด้วย (แตะมุมบนขวาพอดีเป๊ะ)

(5, 5) ผ่านทั้งสอง test อยู่ข้างใน circle แบบมีที่เหลือนิดหน่อย และอยู่บนมุมของ box พอดีเป๊ะ

Exercise 3 AABB มี min=(2, 2) และ max=(6, 6) Ray เริ่มที่ origin=(0, 0) ด้วย direction dir=(1, 1) (ไม่ใช่ unit length แต่ไม่เป็นไรสำหรับแบบฝึกหัดนี้) ใช้ slab test จากหัวข้อ 11 คำนวณช่วง x-slab, ช่วง y-slab และบอกว่า ray ชน box ไหม ที่ t เท่าไหร่
Show answer

X slab: t1 = (2 - 0) / 1 = 2, t2 = (6 - 0) / 1 = 6 ช่วง: [2, 6]

Y slab: ตัวเลขเดียวกันเพราะสมมาตร: t1 = (2 - 0) / 1 = 2, t2 = (6 - 0) / 1 = 6 ช่วง: [2, 6]

Overlap: tMin = max(2, 2) = 2, tMax = min(6, 6) = 6 เพราะ tMin <= tMax และ tMax >= 0 มันคือ hit ที่ t = 2

เช็คความสมเหตุสมผล: จุดที่ชนคือ origin + t*dir = (0 + 2*1, 0 + 2*1) = (2, 2) — มุมของ box ที่ใกล้ origin ที่สุดพอดีเป๊ะ สมเหตุสมผล: direction ของ ray (1,1) ชี้ที่มุม 45 องศาพอดี พุ่งตรงไปหามุมที่ใกล้ origin ที่สุด (หมายเหตุ: เพราะ dir ไม่ใช่ unit length ในที่นี้ t จึงไม่เท่ากับระยะทางจริงที่เดินไป แต่ผลลัพธ์ hit/miss กับจุดที่ชนยังถูกต้องอยู่)

นี่คือคณิตศาสตร์ที่โค้ด gameplay จำนวนมากพึ่งพาอยู่เงียบ ๆ มุมแปลงไปมาระหว่าง degree กับ radian ด้วยสูตรเดียว; unit circle แปลงมุมใด ๆ ให้เป็น direction ผ่าน sin/cos; atan2 แปลงสอง position ใด ๆ ให้เป็นมุมสำหรับเล็ง; และรูปทรงถูก ๆ กลุ่มหนึ่ง — circle, AABB, ray — ครอบคลุมคำถาม collision ส่วนใหญ่ที่เกมถามทุกเฟรม ไม่ต้องท่องจำคำต่อคำ สิ่งที่สำคัญคือการจำรูปแบบของปัญหาได้ ("ฉันมีสอง position และอยากได้มุม" -> atan2; "ฉันมี ray กับ box" -> slab test) แล้วรู้ว่าจะหยิบเครื่องมือไหนมาใช้

← กลับไปหน้ารวมบท