added line sensor obstacle collisions
parent
485aa4557d
commit
0674e5ca2f
62
game.js
62
game.js
|
|
@ -11,6 +11,13 @@ 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");
|
||||||
|
|
@ -98,7 +105,15 @@ 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) {
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
104
gameworld.js
104
gameworld.js
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
sensor.js
33
sensor.js
|
|
@ -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();
|
||||||
|
|
|
||||||
1
todo.md
1
todo.md
|
|
@ -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.
|
||||||
Loading…
Reference in New Issue