前言
从我在酷丁堂上课时,到不在酷丁堂上课用小号看问答,无数大佬前仆后继钟情于3D,然而,他们之中的大部分没有成功
而我为什么把这篇文章发出来呢?我不是猜想,不是征集,我已经有了完整的思路!
现在,我们开始吧。
一,检测玩家与墙的距离
检测玩家与墙的距离,就可以知道墙该画多大
先从最简单的开始,墙是一条线段,玩家是一个点,那么就是计算线段和射线的交点。
这是本章的核心部分
用到的变量解释(用图像思维)
-
p:射线的起点,比如玩家坐标 -
r:射线的方向,比如你看的方向(可以是(cos(angle), sin(angle))) -
q:线段的起点(墙的一端) -
s:线段的方向向量(墙的另一端 - 墙的起点)
这些变量就像是: 你站在 p,拿着激光枪沿着 r 开火 墙从 q 指向 q + s
四个变量都是向量
然后就可以解决几个问题
-
有没有交点?
-
计算射线方向
r和线段方向s的“旋转差” → 用 叉积cross(r, s) -
如果为 0,说明两条线是平行的(永远不会碰上)
-
-
交点的位置在哪?
-
找出两个方向的交叉点,计算出
t:射线走了多少距离才碰到墙 -
再计算出
u:线段上的位置(0 到 1 表示在线段范围内)
-
-
交点合法吗?
-
射线:
t >= 0表示交点在你前方,不是在你后背 -
线段:
0 <= u <= 1表示交点就在墙上,而不是墙延伸出去的地方
-
现在我们开始写代码
假设你站在 (0, 0),朝着 45° 方向发出一束射线,也就是向量 (1, 1)。 你要检测它是否碰到一条墙,墙的两个端点是:
-
A = (2, 0) -
B = (0, 2)
这个墙是从右下到左上的斜线段。
定义变量
Vec2 p = {0, 0}; // 射线起点
Vec2 r = {1, 1}; // 射线方向
Vec2 q = {2, 0}; // 墙的起点
Vec2 s = {-2, 2}; // 墙的方向 = B - A = (0 - 2, 2 - 0)
计算是否平行
rxs = cross(r, s) = 1 * 2 - 1 * (-2) = 2 + 2 = 4
不等于0,说明不平行
接下来计算t和u
qp = q - p = (2 - 0, 0 - 0) = (2, 0)
t = cross(qp, s) / rxs = (2 * 2 - 0 * -2) / 4 = 4 / 4 = 1
u = cross(qp, r) / rxs = (2 * 1 - 0 * 1) / 4 = 2 / 4 = 0.5
判断结果
-
t = 1👉 交点在射线前方 1 个单位处 -
u = 0.5👉 交点在线段中间部分
所以,射线与线段确实相交!
计算交点坐标
intersection.x = p.x + t * r.x = 0 + 1 * 1 = 1
intersection.y = p.y + t * r.y = 0 + 1 * 1 = 1
所以交点位于(1,1)
#include <SDL.h>
#include <cmath>
#include <iostream>
struct Vec2 {
float x, y;
};
struct Line {
float x1, y1, x2, y2;
};
// 叉积
float cross(const Vec2& a, const Vec2& b) {
return a.x * b.y - a.y * b.x;
}
// 射线与线段求交
bool intersectRayLine(const Vec2& rayStart, const Vec2& rayDir, const Line& seg, Vec2& out) {
Vec2 p = rayStart;
Vec2 r = rayDir;
Vec2 q = { seg.x1, seg.y1 };
Vec2 s = { seg.x2 - seg.x1, seg.y2 - seg.y1 };
float rxs = cross(r, s);
if (rxs == 0.0f) return false; // 平行或重合
Vec2 qp = { q.x - p.x, q.y - p.y };
float t = cross(qp, s) / rxs;
float u = cross(qp, r) / rxs;
if (t >= 0.0f && u >= 0.0f && u <= 1.0f) {
out = { p.x + t * r.x, p.y + t * r.y };
return true;
}
return false;
}
int main(int argc, char* argv[]) {
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* win = SDL_CreateWindow("test",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600, SDL_WINDOW_SHOWN);
SDL_Renderer* renderer = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
// 定义射线起点和方向
Vec2 rayStart = { 400, 300 };
Vec2 rayDir = { 1, 0.5 }; // 射线方向
// 定义墙体线段
Line wall = { 200, 200, 600, 450 };
Vec2 intersection;
bool hit = intersectRayLine(rayStart, rayDir, wall, intersection);
bool running = true;
SDL_Event e;
while (running) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) running = false;
}
SDL_SetRenderDrawColor(renderer, 20, 20, 20, 255);
SDL_RenderClear(renderer);
// 渲染墙体
SDL_SetRenderDrawColor(renderer, 200, 200, 200, 255);
SDL_RenderDrawLine(renderer, wall.x1, wall.y1, wall.x2, wall.y2);
// 渲染射线(长一点)
Vec2 rayEnd = { rayStart.x + rayDir.x * 1000, rayStart.y + rayDir.y * 1000 };
SDL_SetRenderDrawColor(renderer, 0, 180, 255, 255);
SDL_RenderDrawLine(renderer, rayStart.x, rayStart.y, rayEnd.x, rayEnd.y);
// 如果有交点,渲染交点
if (hit) {
SDL_SetRenderDrawColor(renderer, 255, 50, 50, 255);
SDL_Rect point = { (int)intersection.x - 3, (int)intersection.y - 3, 6, 6 };
SDL_RenderFillRect(renderer, &point);
}
SDL_RenderPresent(renderer);
SDL_Delay(16);
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}
功能说明
-
玩家在屏幕中央
(400, 300) -
发出一束射线,方向为
(1, 0.5)(右上) -
墙体是一条线段,从
(200, 200)到(600, 450) -
用颜色高亮交点

