Skip to content

Commit

Permalink
Merge pull request #37 from marklovers/raycasting
Browse files Browse the repository at this point in the history
feat: high level raycasting functions
  • Loading branch information
lajbel authored May 23, 2024
2 parents 17f0af3 + 68c038d commit 4f05f6b
Show file tree
Hide file tree
Showing 6 changed files with 760 additions and 42 deletions.
151 changes: 151 additions & 0 deletions examples/mazeRaycastedLight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
kaboom({
scale: 0.5,
background: [0, 0, 0],
});

loadSprite("bean", "sprites/bean.png");
loadSprite("steel", "sprites/steel.png");

const TILE_WIDTH = 64;
const TILE_HEIGHT = TILE_WIDTH;

function createMazeMap(width, height) {
const size = width * height;
function getUnvisitedNeighbours(map, index) {
const n = [];
const x = Math.floor(index / width);
if (x > 1 && map[index - 2] === 2) n.push(index - 2);
if (x < width - 2 && map[index + 2] === 2) n.push(index + 2);
if (index >= 2 * width && map[index - 2 * width] === 2) {
n.push(index - 2 * width);
}
if (index < size - 2 * width && map[index + 2 * width] === 2) {
n.push(index + 2 * width);
}
return n;
}
const map = new Array(size).fill(1, 0, size);
map.forEach((_, index) => {
const x = Math.floor(index / width);
const y = Math.floor(index % width);
if ((x & 1) === 1 && (y & 1) === 1) {
map[index] = 2;
}
});

const stack = [];
const startX = Math.floor(Math.random() * (width - 1)) | 1;
const startY = Math.floor(Math.random() * (height - 1)) | 1;
const start = startX + startY * width;
map[start] = 0;
stack.push(start);
while (stack.length) {
const index = stack.pop();
const neighbours = getUnvisitedNeighbours(map, index);
if (neighbours.length > 0) {
stack.push(index);
const neighbour =
neighbours[Math.floor(neighbours.length * Math.random())];
const between = (index + neighbour) / 2;
map[neighbour] = 0;
map[between] = 0;
stack.push(neighbour);
}
}
return map;
}

function createMazeLevelMap(width, height, options) {
const symbols = options?.symbols || {};
const map = createMazeMap(width, height);
const space = symbols[" "] || " ";
const fence = symbols["#"] || "#";
const detail = [
space,
symbols["╸"] || "╸", // 1
symbols["╹"] || "╹", // 2
symbols["┛"] || "┛", // 3
symbols["╺"] || "╺", // 4
symbols["━"] || "━", // 5
symbols["┗"] || "┗", // 6
symbols["┻"] || "┻", // 7
symbols["╻"] || "╻", // 8
symbols["┓"] || "┓", // 9
symbols["┃"] || "┃", // a
symbols["┫"] || "┫", // b
symbols["┏"] || "┏", // c
symbols["┳"] || "┳", // d
symbols["┣"] || "┣", // e
symbols["╋ "] || "╋ ", // f
];
const symbolMap = options?.detailed
? map.map((s, index) => {
if (s === 0) return space;
const x = Math.floor(index % width);
const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0;
const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0;
const topWall = index >= width && map[index - width] == 1 ? 2 : 0;
const bottomWall =
index < height * width - width && map[index + width] == 1
? 8
: 0;
return detail[leftWall | rightWall | topWall | bottomWall];
})
: map.map((s) => {
return s == 1 ? fence : space;
});
const levelMap = [];
for (let i = 0; i < height; i++) {
levelMap.push(symbolMap.slice(i * width, i * width + width).join(""));
}
return levelMap;
}

const level = addLevel(
createMazeLevelMap(15, 15, {}),
{
tileWidth: TILE_WIDTH,
tileHeight: TILE_HEIGHT,
tiles: {
"#": () => [
sprite("steel"),
tile({ isObstacle: true }),
],
},
},
);

const bean = level.spawn(
[
sprite("bean"),
anchor("center"),
pos(32, 32),
tile(),
agent({ speed: 640, allowDiagonals: true }),
"bean",
],
1,
1,
);

onClick(() => {
const pos = mousePos();
bean.setTarget(vec2(
Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2,
Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2,
));
});

onUpdate(() => {
const pts = [bean.pos];
// This is overkill, since you theoretically only need to shoot rays to grid positions
for (let i = 0; i < 360; i += 1) {
const hit = level.raycast(bean.pos, Vec2.fromAngle(i));
pts.push(hit.point);
}
pts.push(pts[1]);
drawPolygon({
pts: pts,
color: rgb(255, 255, 100),
});
});
201 changes: 201 additions & 0 deletions examples/raycastObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
kaboom();

add([
pos(80, 80),
circle(40),
color(BLUE),
area(),
]);

add([
pos(180, 210),
circle(20),
color(BLUE),
area(),
]);

add([
pos(40, 180),
rect(20, 40),
color(BLUE),
area(),
]);

add([
pos(140, 130),
rect(60, 50),
color(BLUE),
area(),
]);

add([
pos(180, 40),
polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),
color(BLUE),
area(),
]);

add([
pos(280, 130),
polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),
color(BLUE),
area(),
]);

onUpdate(() => {
const shapes = get("shape");
shapes.forEach(s1 => {
if (
shapes.some(s2 =>
s1 !== s2 && s1.getShape().collides(s2.getShape())
)
) {
s1.color = RED;
} else {
s1.color = BLUE;
}
});
});

onDraw("selected", (s) => {
const bbox = s.worldArea().bbox();
drawRect({
pos: bbox.pos.sub(s.pos),
width: bbox.width,
height: bbox.height,
outline: {
color: YELLOW,
width: 1,
},
fill: false,
});
});

onMousePress(() => {
const shapes = get("area");
const pos = mousePos();
const pickList = shapes.filter((shape) => shape.hasPoint(pos));
selection = pickList[pickList.length - 1];
if (selection) {
get("selected").forEach(s => s.unuse("selected"));
selection.use("selected");
}
});

onMouseMove((pos, delta) => {
get("selected").forEach(sel => {
sel.moveBy(delta);
});
get("turn").forEach(laser => {
const oldVec = mousePos().sub(delta).sub(laser.pos);
const newVec = mousePos().sub(laser.pos);
laser.angle += oldVec.angleBetween(newVec);
});
});

onMouseRelease(() => {
get("selected").forEach(s => s.unuse("selected"));
get("turn").forEach(s => s.unuse("turn"));
});

function laser() {
return {
draw() {
drawTriangle({
p1: vec2(-16, -16),
p2: vec2(16, 0),
p3: vec2(-16, 16),
pos: vec2(0, 0),
color: this.color,
});
if (this.showRing || this.is("turn")) {
drawCircle({
pos: vec2(0, 0),
radius: 28,
outline: {
color: RED,
width: 4,
},
fill: false,
});
}
pushTransform();
pushRotate(-this.angle);
const MAX_TRACE_DEPTH = 3;
const MAX_DISTANCE = 400;
let origin = this.pos;
let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE);
let traceDepth = 0;
while (traceDepth < MAX_TRACE_DEPTH) {
const hit = raycast(origin, direction, ["laser"]);
if (!hit) {
drawLine({
p1: origin.sub(this.pos),
p2: origin.add(direction).sub(this.pos),
width: 1,
color: this.color,
});
break;
}
const pos = hit.point.sub(this.pos);
// Draw hit point
drawCircle({
pos: pos,
radius: 4,
color: this.color,
});
// Draw hit normal
drawLine({
p1: pos,
p2: pos.add(hit.normal.scale(20)),
width: 1,
color: BLUE,
});
// Draw hit distance
drawLine({
p1: origin.sub(this.pos),
p2: pos,
width: 1,
color: this.color,
});
// Offset the point slightly, otherwise it might be too close to the surface
// and give internal reflections
origin = hit.point.add(hit.normal.scale(0.001));
// Reflect vector
direction = direction.reflect(hit.normal);
traceDepth++;
}
popTransform();
},
showRing: false,
};
}

const ray = add([
pos(150, 270),
rotate(-45),
anchor("center"),
rect(64, 64),
area(),
laser(0),
color(RED),
opacity(0.0),
"laser",
]);

get("laser").forEach(laser => {
laser.onHover(() => {
laser.showRing = true;
});
laser.onHoverEnd(() => {
laser.showRing = false;
});
laser.onClick(() => {
get("selected").forEach(s => s.unuse("selected"));
if (laser.pos.sub(mousePos()).slen() > 28 * 28) {
laser.use("turn");
} else {
laser.use("selected");
}
});
});
Loading

0 comments on commit 4f05f6b

Please sign in to comment.