#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <SPI.h>
// ===== TFT 螢幕設定 =====
#define TFT_CS 10
#define TFT_DC 9
#define TFT_RST 8
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
// ===== 螢幕設定 =====
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320
// ===== 地圖設定(5x5 pixels per tile)=====
#define TILE_SIZE 5
#define MAP_WIDTH (SCREEN_WIDTH / TILE_SIZE)
#define MAP_HEIGHT (SCREEN_HEIGHT / TILE_SIZE)
uint8_t mapData[MAP_HEIGHT][MAP_WIDTH] = {
// 簡化範例,只放幾行
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
};
// ===== 角色設定 =====
int playerPixelX = 60, playerPixelY = 60;
int prevPlayerX = playerPixelX, prevPlayerY = playerPixelY;
int moveSpeed = 8;
// ===== 準心設定(固定半徑)=====
float aimAngle = 0;
int aimDistance = 30;
int aimPixelX, aimPixelY;
int prevAimX = 0, prevAimY = 0;
// ===== 子彈設定 =====
#define MAX_BULLETS 10
#define BULLET_SPEED 4
bool visionMask[MAP_HEIGHT][MAP_WIDTH] = {false};
struct Bullet {
int x, y;
int dx, dy;
bool active;
};
Bullet bullets[MAX_BULLETS];
// ===== 控制腳位 =====
#define JOY_X 1
#define JOY_Y 2
#define BTN_CW 3
#define BTN_CCW 4
#define BTN_SHOOT 20
#define TILE_SIZE 5
void initBullets() {
for (int i = 0; i < MAX_BULLETS; i++) {
bullets[i].active = false;
}
}
// === 根據像素位置,取得對應地圖格的顏色 ===
uint16_t getTileColor(int gridX, int gridY) {
if (gridX < 0 || gridX >= MAP_WIDTH || gridY < 0 || gridY >= MAP_HEIGHT) {
return ILI9341_BLACK;
}
if (mapData[gridY][gridX] == 1)
return ILI9341_WHITE; // 牆壁
else
return ILI9341_BLACK; // 地板
}
void handleShooting() {
static bool lastState = HIGH;
bool currentState = digitalRead(BTN_SHOOT);
if (lastState == HIGH && currentState == LOW) {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullets[i].active) {
bullets[i].x = playerPixelX;
bullets[i].y = playerPixelY;
bullets[i].dx = cos(aimAngle) * BULLET_SPEED;
bullets[i].dy = sin(aimAngle) * BULLET_SPEED;
bullets[i].active = true;
break;
}
}
}
lastState = currentState;
}
void drawMap() {
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
if (mapData[y][x] == 1) {
tft.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE, ILI9341_WHITE); // 牆壁
} else {
tft.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE, ILI9341_BLACK); // 地板
}
}
}
}
void updateBullets() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullets[i].active) {
// 擦除舊位置
int eraseGridX = bullets[i].x / TILE_SIZE;
int eraseGridY = bullets[i].y / TILE_SIZE;
tft.fillRect(eraseGridX * TILE_SIZE, eraseGridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, getTileColor(eraseGridX, eraseGridY));
// 移動
bullets[i].x += bullets[i].dx;
bullets[i].y += bullets[i].dy;
// 出界或撞牆判斷
int gridX = bullets[i].x / TILE_SIZE;
int gridY = bullets[i].y / TILE_SIZE;
if (bullets[i].x < 0 || bullets[i].x >= SCREEN_WIDTH ||
bullets[i].y < 0 || bullets[i].y >= SCREEN_HEIGHT ||
mapData[gridY][gridX] == 1) {
bullets[i].active = false;
continue;
}
// 畫子彈
tft.fillRect(gridX * TILE_SIZE, gridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, ILI9341_YELLOW);
}
}
}
// ===== 移動 & 畫圖 =====
void updatePlayer() {
int xVal = analogRead(JOY_X);
int yVal = analogRead(JOY_Y);
int dx = 0, dy = 0;
if (xVal < 1000) dy = -moveSpeed;
if (xVal > 3000) dy = moveSpeed;
if (yVal < 1000) dx = moveSpeed;
if (yVal > 3000) dx = -moveSpeed;
prevPlayerX = playerPixelX;
prevPlayerY = playerPixelY;
int nextX = playerPixelX + dx;
int nextY = playerPixelY + dy;
int gridX = nextX / TILE_SIZE;
int gridY = nextY / TILE_SIZE;
// 檢查下一格是否是牆(1 = 牆,0 = 地板)
if (mapData[gridY][gridX] == 0) {
playerPixelX = nextX;
playerPixelY = nextY;
}
}
void eraseTileAt(int pixelX, int pixelY) {
int gridX = pixelX / TILE_SIZE;
int gridY = pixelY / TILE_SIZE;
tft.fillRect(gridX * TILE_SIZE, gridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, getTileColor(gridX, gridY));
}
void drawPlayer() {
int prevGridX = prevPlayerX / TILE_SIZE;
int prevGridY = prevPlayerY / TILE_SIZE;
int gridX = playerPixelX / TILE_SIZE;
int gridY = playerPixelY / TILE_SIZE;
// 先還原舊格(防止角色殘影)
if (prevGridX >= 0 && prevGridX < MAP_WIDTH && prevGridY >= 0 && prevGridY < MAP_HEIGHT) {
tft.fillRect(prevGridX * TILE_SIZE, prevGridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, getTileColor(prevGridX, prevGridY));
}
// 畫新格(不管有沒有移動,都畫)
if (gridX >= 0 && gridX < MAP_WIDTH && gridY >= 0 && gridY < MAP_HEIGHT) {
tft.fillRect(gridX * TILE_SIZE, gridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, ILI9341_RED);
}
// 更新 prev 記錄
prevPlayerX = playerPixelX;
prevPlayerY = playerPixelY;
}
void updateAim() {
if (digitalRead(BTN_CW) == LOW) {
aimAngle += 0.1;
}
if (digitalRead(BTN_CCW) == LOW) {
aimAngle -= 0.1;
}
prevAimX = aimPixelX;
prevAimY = aimPixelY;
aimPixelX = playerPixelX + cos(aimAngle) * aimDistance;
aimPixelY = playerPixelY + sin(aimAngle) * aimDistance;
}
void drawAim() {
// 擦除舊準心位置(正確還原背景)
int prevGridX = prevAimX / TILE_SIZE;
int prevGridY = prevAimY / TILE_SIZE;
if (prevGridX >= 0 && prevGridX < MAP_WIDTH && prevGridY >= 0 && prevGridY < MAP_HEIGHT) {
tft.fillRect(prevGridX * TILE_SIZE, prevGridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, getTileColor(prevGridX, prevGridY));
}
// 計算新的準心座標
aimPixelX = playerPixelX + cos(aimAngle) * aimDistance;
aimPixelY = playerPixelY + sin(aimAngle) * aimDistance;
int gridX = aimPixelX / TILE_SIZE;
int gridY = aimPixelY / TILE_SIZE;
// 畫新的準心
if (gridX >= 0 && gridX < MAP_WIDTH && gridY >= 0 && gridY < MAP_HEIGHT) {
tft.fillRect(gridX * TILE_SIZE, gridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, ILI9341_CYAN);
}
// 記錄為下次清除用
prevAimX = aimPixelX;
prevAimY = aimPixelY;
}
void setup() {
tft.begin();
tft.fillScreen(ILI9341_BLACK);
drawMap(); // ✅ 一次就好,背景靜止不變時用這種方式
pinMode(BTN_CW, INPUT_PULLUP);
pinMode(BTN_CCW, INPUT_PULLUP);
pinMode(BTN_SHOOT, INPUT_PULLUP);
initBullets();
}
void loop() {
updatePlayer();
updateAim();
drawVisionCone(); // 畫視角會蓋地板,但不會動到角色
drawAim(); // 畫準心
drawPlayer(); // ✅ 每幀畫一次主角
handleShooting();
updateBullets();
delay(10);
}
#define FOV_ANGLE 0.436 // 50 度 = 50 * PI / 180 ≈ 0.436 弧度
#define FOV_DISTANCE 200 // 可視距離(像素)
void drawVisionCone() {
// 清除上一幀視角
for (int y = 0; y < MAP_HEIGHT; y++) {
for (int x = 0; x < MAP_WIDTH; x++) {
if (visionMask[y][x]) {
tft.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE, getTileColor(x, y));
visionMask[y][x] = false;
}
}
}
// 畫新的視角扇形(會被牆擋住)
float angleStart = aimAngle - FOV_ANGLE;
float angleEnd = aimAngle + FOV_ANGLE;
for (float angle = angleStart; angle <= angleEnd; angle += 0.05) {
for (int r = 5; r < FOV_DISTANCE; r += 1) {
int x = playerPixelX + cos(angle) * r;
int y = playerPixelY + sin(angle) * r;
int gridX = x / TILE_SIZE;
int gridY = y / TILE_SIZE;
// 超出地圖邊界就停
if (gridX < 0 || gridX >= MAP_WIDTH || gridY < 0 || gridY >= MAP_HEIGHT) break;
// 畫這一格(牆也會畫)
tft.fillRect(gridX * TILE_SIZE, gridY * TILE_SIZE, TILE_SIZE, TILE_SIZE, ILI9341_YELLOW);
visionMask[gridY][gridX] = true;
// ✅ 若是牆,畫完就停 → 後面不再畫!
if (mapData[gridY][gridX] == 1) break;
}
}
}