added line sensor obstacle collisions

master
Jake 2025-03-27 16:05:41 +08:00
parent 485aa4557d
commit 0674e5ca2f
4 changed files with 184 additions and 20 deletions

66
game.js
View File

@ -11,10 +11,17 @@ let pyodideWorker = startPyodideWorker();
let robots = createInitialRobots(); let robots = createInitialRobots();
let paused = false; let paused = false;
let scale = 1; // Zoom level
let offsetX = 0; // Pan X
let offsetY = 0; // Pan Y
let isPanning = false;
let startX, startY; // Mouse start positions
// ✅ Function to create the Pyodide Worker // ✅ Function to create the Pyodide Worker
function startPyodideWorker() { function startPyodideWorker() {
const worker = new Worker("pyodide-worker.js"); const worker = new Worker("pyodide-worker.js");
// ✅ Reattach the event listener when a new worker is created // ✅ Reattach the event listener when a new worker is created
worker.onmessage = (event) => { worker.onmessage = (event) => {
if (paused) return; if (paused) return;
@ -98,9 +105,17 @@ function resetGame() {
// ✅ Game Loop // ✅ Game Loop
function gameLoop() { function gameLoop() {
if (!paused) { if (!paused) {
ctx.clearRect(0, 0, gameCanvas.width, gameCanvas.height); ctx.resetTransform();
// Fill the entire visible canvas to remove artifacts
ctx.fillStyle = "#DDD";
ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
ctx.translate(offsetX, offsetY); // Apply panning
ctx.scale(scale, scale); // Apply zooming
gameWorld.draw(ctx); gameWorld.draw(ctx);
for (let id in robots) { for (let id in robots) {
robots[id].update(ctx, gameWorld); robots[id].update(ctx, gameWorld);
} }
@ -108,6 +123,7 @@ function gameLoop() {
requestAnimationFrame(gameLoop); requestAnimationFrame(gameLoop);
} }
// Start game loop // Start game loop
gameLoop(); gameLoop();
@ -124,3 +140,47 @@ document.getElementById("compile-button").addEventListener("click", () => {
document.getElementById("pause-button").addEventListener("click", togglePause); document.getElementById("pause-button").addEventListener("click", togglePause);
document.getElementById("reset-button").addEventListener("click", resetGame); document.getElementById("reset-button").addEventListener("click", resetGame);
gameCanvas.addEventListener("wheel", (event) => {
event.preventDefault();
const scaleFactor = 1.1;
const mouseX = event.offsetX;
const mouseY = event.offsetY;
// Convert mouse coordinates to world coordinates (before zoom)
const worldX = (mouseX - offsetX) / scale;
const worldY = (mouseY - offsetY) / scale;
// Apply zoom
if (event.deltaY < 0) {
scale *= scaleFactor; // Zoom in
} else {
scale /= scaleFactor; // Zoom out
}
// Keep zoom within limits
scale = Math.max(0.5, Math.min(3, scale));
// Adjust offset so zooming is centered at mouse position
offsetX = mouseX - worldX * scale;
offsetY = mouseY - worldY * scale;
});
gameCanvas.addEventListener("mousedown", (event) => {
isPanning = true;
startX = event.clientX - offsetX;
startY = event.clientY - offsetY;
});
gameCanvas.addEventListener("mousemove", (event) => {
if (!isPanning) return;
offsetX = event.clientX - startX;
offsetY = event.clientY - startY;
});
gameCanvas.addEventListener("mouseup", () => {
isPanning = false;
});

View File

@ -1,16 +1,33 @@
export class GameWorld { export class GameWorld {
constructor() { constructor() {
let polygon1 = [
{ x: 80, y: 100 }, // Vertex 1
{ x: 200, y: 100 }, // Vertex 2
{ x: 200, y: 200 }, // Vertex 3
{ x: 100, y: 200 } // Vertex 4
];
let polygon2 = [
{ x: 300, y: 400 }, // Vertex 1
{ x: 320, y: 450 }, // Vertex 2
{ x: 250, y: 550 }, // Vertex 3
{ x: 280, y: 420 } // Vertex 4
];
this.obstacles = [ this.obstacles = [
{ x: 300, y: 300, width: 50, height: 50 }, // Example obstacle polygon1, // Example obstacle
{ x: 500, y: 400, width: 70, height: 70 } // Another obstacle polygon2 // Another obstacle
]; ];
} }
// Check if a point (x, y) intersects with any obstacles // Check if a point (x, y) intersects with any obstacles
isObstacle(x, y) { isObstacle(x, y) {
return this.obstacles.some( // return this.obstacles.some(
obj => x > obj.x && x < obj.x + obj.width && y > obj.y && y < obj.y + obj.height // obj => x > obj.x && x < obj.x + obj.width && y > obj.y && y < obj.y + obj.height
); // );
return true;
} }
// Return the floor color based on (x, y) coordinates // Return the floor color based on (x, y) coordinates
@ -21,13 +38,76 @@ export class GameWorld {
// Draw the game world (e.g., obstacles, background) // Draw the game world (e.g., obstacles, background)
draw(ctx) { draw(ctx) {
// Draw obstacles // Draw obstacles
ctx.fillStyle = "gray"; // Obstacle color ctx.strokeStyle = "gray"; // Obstacle outline color
this.obstacles.forEach(obstacle => { ctx.lineWidth = 2; // Optional: to make the outline thicker
ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height);
});
// Optionally, draw the grid or other visual elements (e.g., floor color patterns) this.obstacles.forEach(obstacle => {
ctx.fillStyle = "lightgray"; // Example background ctx.beginPath();
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); // Draw background ctx.moveTo(obstacle[0].x, obstacle[0].y); // Start at the first vertex
// Loop through the rest of the vertices and draw lines
for (let i = 1; i < obstacle.length; i++) {
ctx.lineTo(obstacle[i].x, obstacle[i].y);
}
// Close the path to form a closed polygon
ctx.closePath();
ctx.stroke(); // Draw the outline
});
} }
rayCast(startX, startY, endX, endY) {
let closestIntersection = null;
// Loop through all obstacles
for (let i = 0; i < this.obstacles.length; i++) {
let obstacle = this.obstacles[i];
return this.lineIntersectsPolygon(startX, startY, endX, endY, obstacle);
}
return closestIntersection;
}
lineIntersection(line1Start, line1End, line2Start, line2End) {
let denom = (line1Start.x - line1End.x) * (line2Start.y - line2End.y) -
(line1Start.y - line1End.y) * (line2Start.x - line2End.x);
if (denom === 0) return null; // Lines are parallel
let t = ((line1Start.x - line2Start.x) * (line2Start.y - line2End.y) -
(line1Start.y - line2Start.y) * (line2Start.x - line2End.x)) / denom;
let u = ((line1Start.x - line2Start.x) * (line1Start.y - line1End.y) -
(line1Start.y - line2Start.y) * (line1Start.x - line1End.x)) / denom;
if (t >= 0 && t <= 1 && u >= 0 && u <= 1) {
let ix = line1Start.x + t * (line1End.x - line1Start.x);
let iy = line1Start.y + t * (line1End.y - line1Start.y);
return { x: ix, y: iy };
}
return null; // No intersection
}
lineIntersectsPolygon(startX, startY, endX, endY, polygon) {
let lineStart = { x: startX, y: startY };
let lineEnd = { x: endX, y: endY };
// Loop through all edges of the polygon
for (let i = 0; i < polygon.length; i++) {
let currentVertex = polygon[i];
let nextVertex = polygon[(i + 1) % polygon.length]; // Loop back to the first vertex
let intersection = this.lineIntersection(lineStart, lineEnd, currentVertex, nextVertex);
if (intersection) {
return intersection; // Return the first intersection found
}
}
return null; // No intersection with any edge of the polygon
}
} }

View File

@ -4,7 +4,9 @@ export class Sensor {
this.value = null; // Last recorded value this.value = null; // Last recorded value
this.offsetAngle = offsetAngle; this.offsetAngle = offsetAngle;
this.offsetDistance = offsetDistance; this.offsetDistance = offsetDistance;
this.hitX = null;
this.hitY = null;
this.hitObject = null;
} }
@ -45,20 +47,31 @@ export class RaycastSensor extends Sensor {
read(robot, gameWorld) { read(robot, gameWorld) {
//console.log(robot); //console.log(robot);
//console.log(gameWorld); //console.log(gameWorld);
this.updatePosition();
const angleInRadians = (this.robot.direction + this.angle) * Math.PI / 180; const angleInRadians = (this.robot.direction + this.angle) * Math.PI / 180;
const x = this.robot.x + Math.cos(angleInRadians) * this.distance; const x = this.robot.x + Math.cos(angleInRadians) * this.distance;
const y = this.robot.y + Math.sin(angleInRadians) * this.distance; const y = this.robot.y + Math.sin(angleInRadians) * this.distance;
// Ensure gameWorld is available and properly passed to the sensor // Ensure gameWorld is available and properly passed to the sensor
if (gameWorld.isObstacle(x, y)) { let hitPos = gameWorld.rayCast(this.startX, this.startY, this.endX, this.endY);
//console.log("Obstacle detected!"); if (hitPos != null) {
this.hitX = hitPos.x;
this.hitY = hitPos.y;
this.endX = this.hitX;
this.endY = this.hitY;
} else { } else {
//console.log("Clear path!"); this.hitX = null;
this.hitY = null;
} }
// console.log("Obstacle detected!");
// } else {
// console.log("Clear path!");
// }
} }
draw(ctx) { draw(ctx) {
this.updatePosition();
ctx.strokeStyle = "lime"; ctx.strokeStyle = "lime";
ctx.fillStyle = "green" ctx.fillStyle = "green"
@ -68,6 +81,16 @@ export class RaycastSensor extends Sensor {
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
if (this.hitX != null) {
ctx.strokeStyle = "red";
ctx.fillStyle = "red"
ctx.beginPath();
ctx.arc(this.hitX, this.hitY, 2, 0, 2 * Math.PI);
ctx.fillStyle = "red";
ctx.fill();
ctx.stroke();
}
ctx.strokeStyle = "yellow"; ctx.strokeStyle = "yellow";
ctx.beginPath(); ctx.beginPath();

View File

@ -6,6 +6,7 @@ Add pan and zoom to canvas
IN PROGRESS IN PROGRESS
DONE DONE
Add line sensor object collisions
Add robot sensors that detect things in game world, color sense and distance sense for start Add robot sensors that detect things in game world, color sense and distance sense for start
Add Pause Button Add Pause Button
Add reset button. Add reset button.