Skip to content

Commit 68c038d

Browse files
author
Marc Flerackers
committed
high level raycasting functions
1 parent 17f0af3 commit 68c038d

File tree

6 files changed

+760
-42
lines changed

6 files changed

+760
-42
lines changed

examples/mazeRaycastedLight.js

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
kaboom({
2+
scale: 0.5,
3+
background: [0, 0, 0],
4+
});
5+
6+
loadSprite("bean", "sprites/bean.png");
7+
loadSprite("steel", "sprites/steel.png");
8+
9+
const TILE_WIDTH = 64;
10+
const TILE_HEIGHT = TILE_WIDTH;
11+
12+
function createMazeMap(width, height) {
13+
const size = width * height;
14+
function getUnvisitedNeighbours(map, index) {
15+
const n = [];
16+
const x = Math.floor(index / width);
17+
if (x > 1 && map[index - 2] === 2) n.push(index - 2);
18+
if (x < width - 2 && map[index + 2] === 2) n.push(index + 2);
19+
if (index >= 2 * width && map[index - 2 * width] === 2) {
20+
n.push(index - 2 * width);
21+
}
22+
if (index < size - 2 * width && map[index + 2 * width] === 2) {
23+
n.push(index + 2 * width);
24+
}
25+
return n;
26+
}
27+
const map = new Array(size).fill(1, 0, size);
28+
map.forEach((_, index) => {
29+
const x = Math.floor(index / width);
30+
const y = Math.floor(index % width);
31+
if ((x & 1) === 1 && (y & 1) === 1) {
32+
map[index] = 2;
33+
}
34+
});
35+
36+
const stack = [];
37+
const startX = Math.floor(Math.random() * (width - 1)) | 1;
38+
const startY = Math.floor(Math.random() * (height - 1)) | 1;
39+
const start = startX + startY * width;
40+
map[start] = 0;
41+
stack.push(start);
42+
while (stack.length) {
43+
const index = stack.pop();
44+
const neighbours = getUnvisitedNeighbours(map, index);
45+
if (neighbours.length > 0) {
46+
stack.push(index);
47+
const neighbour =
48+
neighbours[Math.floor(neighbours.length * Math.random())];
49+
const between = (index + neighbour) / 2;
50+
map[neighbour] = 0;
51+
map[between] = 0;
52+
stack.push(neighbour);
53+
}
54+
}
55+
return map;
56+
}
57+
58+
function createMazeLevelMap(width, height, options) {
59+
const symbols = options?.symbols || {};
60+
const map = createMazeMap(width, height);
61+
const space = symbols[" "] || " ";
62+
const fence = symbols["#"] || "#";
63+
const detail = [
64+
space,
65+
symbols["╸"] || "╸", // 1
66+
symbols["╹"] || "╹", // 2
67+
symbols["┛"] || "┛", // 3
68+
symbols["╺"] || "╺", // 4
69+
symbols["━"] || "━", // 5
70+
symbols["┗"] || "┗", // 6
71+
symbols["┻"] || "┻", // 7
72+
symbols["╻"] || "╻", // 8
73+
symbols["┓"] || "┓", // 9
74+
symbols["┃"] || "┃", // a
75+
symbols["┫"] || "┫", // b
76+
symbols["┏"] || "┏", // c
77+
symbols["┳"] || "┳", // d
78+
symbols["┣"] || "┣", // e
79+
symbols["╋ "] || "╋ ", // f
80+
];
81+
const symbolMap = options?.detailed
82+
? map.map((s, index) => {
83+
if (s === 0) return space;
84+
const x = Math.floor(index % width);
85+
const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0;
86+
const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0;
87+
const topWall = index >= width && map[index - width] == 1 ? 2 : 0;
88+
const bottomWall =
89+
index < height * width - width && map[index + width] == 1
90+
? 8
91+
: 0;
92+
return detail[leftWall | rightWall | topWall | bottomWall];
93+
})
94+
: map.map((s) => {
95+
return s == 1 ? fence : space;
96+
});
97+
const levelMap = [];
98+
for (let i = 0; i < height; i++) {
99+
levelMap.push(symbolMap.slice(i * width, i * width + width).join(""));
100+
}
101+
return levelMap;
102+
}
103+
104+
const level = addLevel(
105+
createMazeLevelMap(15, 15, {}),
106+
{
107+
tileWidth: TILE_WIDTH,
108+
tileHeight: TILE_HEIGHT,
109+
tiles: {
110+
"#": () => [
111+
sprite("steel"),
112+
tile({ isObstacle: true }),
113+
],
114+
},
115+
},
116+
);
117+
118+
const bean = level.spawn(
119+
[
120+
sprite("bean"),
121+
anchor("center"),
122+
pos(32, 32),
123+
tile(),
124+
agent({ speed: 640, allowDiagonals: true }),
125+
"bean",
126+
],
127+
1,
128+
1,
129+
);
130+
131+
onClick(() => {
132+
const pos = mousePos();
133+
bean.setTarget(vec2(
134+
Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2,
135+
Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2,
136+
));
137+
});
138+
139+
onUpdate(() => {
140+
const pts = [bean.pos];
141+
// This is overkill, since you theoretically only need to shoot rays to grid positions
142+
for (let i = 0; i < 360; i += 1) {
143+
const hit = level.raycast(bean.pos, Vec2.fromAngle(i));
144+
pts.push(hit.point);
145+
}
146+
pts.push(pts[1]);
147+
drawPolygon({
148+
pts: pts,
149+
color: rgb(255, 255, 100),
150+
});
151+
});

examples/raycastObject.js

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
kaboom();
2+
3+
add([
4+
pos(80, 80),
5+
circle(40),
6+
color(BLUE),
7+
area(),
8+
]);
9+
10+
add([
11+
pos(180, 210),
12+
circle(20),
13+
color(BLUE),
14+
area(),
15+
]);
16+
17+
add([
18+
pos(40, 180),
19+
rect(20, 40),
20+
color(BLUE),
21+
area(),
22+
]);
23+
24+
add([
25+
pos(140, 130),
26+
rect(60, 50),
27+
color(BLUE),
28+
area(),
29+
]);
30+
31+
add([
32+
pos(180, 40),
33+
polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),
34+
color(BLUE),
35+
area(),
36+
]);
37+
38+
add([
39+
pos(280, 130),
40+
polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),
41+
color(BLUE),
42+
area(),
43+
]);
44+
45+
onUpdate(() => {
46+
const shapes = get("shape");
47+
shapes.forEach(s1 => {
48+
if (
49+
shapes.some(s2 =>
50+
s1 !== s2 && s1.getShape().collides(s2.getShape())
51+
)
52+
) {
53+
s1.color = RED;
54+
} else {
55+
s1.color = BLUE;
56+
}
57+
});
58+
});
59+
60+
onDraw("selected", (s) => {
61+
const bbox = s.worldArea().bbox();
62+
drawRect({
63+
pos: bbox.pos.sub(s.pos),
64+
width: bbox.width,
65+
height: bbox.height,
66+
outline: {
67+
color: YELLOW,
68+
width: 1,
69+
},
70+
fill: false,
71+
});
72+
});
73+
74+
onMousePress(() => {
75+
const shapes = get("area");
76+
const pos = mousePos();
77+
const pickList = shapes.filter((shape) => shape.hasPoint(pos));
78+
selection = pickList[pickList.length - 1];
79+
if (selection) {
80+
get("selected").forEach(s => s.unuse("selected"));
81+
selection.use("selected");
82+
}
83+
});
84+
85+
onMouseMove((pos, delta) => {
86+
get("selected").forEach(sel => {
87+
sel.moveBy(delta);
88+
});
89+
get("turn").forEach(laser => {
90+
const oldVec = mousePos().sub(delta).sub(laser.pos);
91+
const newVec = mousePos().sub(laser.pos);
92+
laser.angle += oldVec.angleBetween(newVec);
93+
});
94+
});
95+
96+
onMouseRelease(() => {
97+
get("selected").forEach(s => s.unuse("selected"));
98+
get("turn").forEach(s => s.unuse("turn"));
99+
});
100+
101+
function laser() {
102+
return {
103+
draw() {
104+
drawTriangle({
105+
p1: vec2(-16, -16),
106+
p2: vec2(16, 0),
107+
p3: vec2(-16, 16),
108+
pos: vec2(0, 0),
109+
color: this.color,
110+
});
111+
if (this.showRing || this.is("turn")) {
112+
drawCircle({
113+
pos: vec2(0, 0),
114+
radius: 28,
115+
outline: {
116+
color: RED,
117+
width: 4,
118+
},
119+
fill: false,
120+
});
121+
}
122+
pushTransform();
123+
pushRotate(-this.angle);
124+
const MAX_TRACE_DEPTH = 3;
125+
const MAX_DISTANCE = 400;
126+
let origin = this.pos;
127+
let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE);
128+
let traceDepth = 0;
129+
while (traceDepth < MAX_TRACE_DEPTH) {
130+
const hit = raycast(origin, direction, ["laser"]);
131+
if (!hit) {
132+
drawLine({
133+
p1: origin.sub(this.pos),
134+
p2: origin.add(direction).sub(this.pos),
135+
width: 1,
136+
color: this.color,
137+
});
138+
break;
139+
}
140+
const pos = hit.point.sub(this.pos);
141+
// Draw hit point
142+
drawCircle({
143+
pos: pos,
144+
radius: 4,
145+
color: this.color,
146+
});
147+
// Draw hit normal
148+
drawLine({
149+
p1: pos,
150+
p2: pos.add(hit.normal.scale(20)),
151+
width: 1,
152+
color: BLUE,
153+
});
154+
// Draw hit distance
155+
drawLine({
156+
p1: origin.sub(this.pos),
157+
p2: pos,
158+
width: 1,
159+
color: this.color,
160+
});
161+
// Offset the point slightly, otherwise it might be too close to the surface
162+
// and give internal reflections
163+
origin = hit.point.add(hit.normal.scale(0.001));
164+
// Reflect vector
165+
direction = direction.reflect(hit.normal);
166+
traceDepth++;
167+
}
168+
popTransform();
169+
},
170+
showRing: false,
171+
};
172+
}
173+
174+
const ray = add([
175+
pos(150, 270),
176+
rotate(-45),
177+
anchor("center"),
178+
rect(64, 64),
179+
area(),
180+
laser(0),
181+
color(RED),
182+
opacity(0.0),
183+
"laser",
184+
]);
185+
186+
get("laser").forEach(laser => {
187+
laser.onHover(() => {
188+
laser.showRing = true;
189+
});
190+
laser.onHoverEnd(() => {
191+
laser.showRing = false;
192+
});
193+
laser.onClick(() => {
194+
get("selected").forEach(s => s.unuse("selected"));
195+
if (laser.pos.sub(mousePos()).slen() > 28 * 28) {
196+
laser.use("turn");
197+
} else {
198+
laser.use("selected");
199+
}
200+
});
201+
});

0 commit comments

Comments
 (0)