-- raycast_shooter.lua -- Простой 3D-шутер для OpenComputers (raycasting, как в Wolfenstein 3D), -- построенный поверх библиотеки pixelgfx.lua (полу-блочная суб-пиксельная -- графика с diff-перерисовкой). -- -- Управление: -- W / S — вперёд / назад -- A / D — страйф влево / вправо -- Left/Right — поворот камеры -- Space — выстрел -- Esc — выход -- -- Файл pixelgfx.lua должен лежать рядом (или быть доступен через require). local component = require("component") local computer = require("computer") local event = require("event") local keyboard = require("keyboard") local term = require("term") local keys = keyboard.keys local pixelgfx = require("pixelgfx") local gpu = component.gpu ---------------------------------------------------------------------- -- Настройка экрана / канваса ---------------------------------------------------------------------- local maxW, maxH = gpu.maxResolution() local HUD_ROWS = 3 local W_CHARS = math.min(80, maxW) -- ширина игрового поля в пикселях/символах local canvasRows = math.max(10, maxH - HUD_ROWS) -- символьных строк под канвас local H_PIXELS = canvasRows * 2 -- высота игрового поля в пикселях gpu.setResolution(W_CHARS, canvasRows + HUD_ROWS) term.clear() local canvas = pixelgfx.new(gpu, 1, 1, W_CHARS, H_PIXELS) local SCREEN_W, SCREEN_H = W_CHARS, H_PIXELS ---------------------------------------------------------------------- -- Карта. '.' — пусто, любой другой символ (кроме E/P) — стена. -- 'E' — точка спавна врага, 'P' — стартовая позиция игрока. ---------------------------------------------------------------------- local MAP_SRC = { "111111111111111111", "1..........E.....1", "1..111....111....1", "1..1.........1...1", "1..1....P....1...1", "1..1.........1...1", "1..111....111..E.1", "1.................1", "1..E....2222......1", "1.......2..2......1", "1.......2222....E.1", "1..................1", "111111111111111111", } local MAPGRID = {} local enemies = {} local player = { x = 2.5, y = 2.5, angle = 0, health = 100, ammo = 999 } for y = 1, #MAP_SRC do MAPGRID[y] = {} for x = 1, #MAP_SRC[y] do local ch = MAP_SRC[y]:sub(x, x) if ch == "E" then table.insert(enemies, { x = x - 0.5, y = y - 0.5, health = 3, alive = true, speed = 0.9, }) ch = "." elseif ch == "P" then player.x, player.y = x - 0.5, y - 0.5 ch = "." end MAPGRID[y][x] = ch end end local function isWall(mx, my) local row = MAPGRID[my] if not row then return true end local c = row[mx] if c == nil then return true end return c ~= "." end ---------------------------------------------------------------------- -- Цвета ---------------------------------------------------------------------- local WALL_COLORS = { ["1"] = 0xA0522D, -- кирпич ["2"] = 0x808080, -- камень } local DEFAULT_WALL_COLOR = 0x555555 local CEIL_COLOR = 0x1A1A2E local FLOOR_COLOR = 0x2B2B1E local ENEMY_COLOR = 0x2ECC71 local ENEMY_HIT_COLOR = 0xE74C3C local function shade(color, k) if k < 0 then k = 0 elseif k > 1 then k = 1 end local r = math.floor(((color >> 16) & 0xFF) * k) local g = math.floor(((color >> 8) & 0xFF) * k) local b = math.floor((color & 0xFF) * k) return (r << 16) | (g << 8) | b end ---------------------------------------------------------------------- -- Параметры игры ---------------------------------------------------------------------- local FOV = math.pi / 3 local MAX_DEPTH = 18 local MOVE_SPEED = 3.2 local ROT_SPEED = 2.6 local PLAYER_RADIUS = 0.22 local SHOOT_RANGE = MAX_DEPTH local SHOOT_TOLERANCE = 0.05 -- радианы, "толщина" прицела local SHOOT_COOLDOWN = 0.25 local zbuffer = {} local score = 0 local running = true local gameOverMsg = nil ---------------------------------------------------------------------- -- Raycasting (DDA) ---------------------------------------------------------------------- local function castRay(px, py, angle) local dirX, dirY = math.cos(angle), math.sin(angle) local mapX, mapY = math.floor(px), math.floor(py) local deltaDistX = (dirX == 0) and 1e30 or math.abs(1 / dirX) local deltaDistY = (dirY == 0) and 1e30 or math.abs(1 / dirY) local stepX, sideDistX local stepY, sideDistY if dirX < 0 then stepX = -1 sideDistX = (px - mapX) * deltaDistX else stepX = 1 sideDistX = (mapX + 1 - px) * deltaDistX end if dirY < 0 then stepY = -1 sideDistY = (py - mapY) * deltaDistY else stepY = 1 sideDistY = (mapY + 1 - py) * deltaDistY end local side, hitChar for _ = 1, 64 do if sideDistX < sideDistY then sideDistX = sideDistX + deltaDistX mapX = mapX + stepX side = 0 else sideDistY = sideDistY + deltaDistY mapY = mapY + stepY side = 1 end local row = MAPGRID[mapY] local c = row and row[mapX] if c == nil or c ~= "." then hitChar = c or "1" break end end local dist if side == 0 then dist = (mapX - px + (1 - stepX) / 2) / dirX else dist = (mapY - py + (1 - stepY) / 2) / dirY end return dist, side, hitChar end ---------------------------------------------------------------------- -- Отрисовка сцены ---------------------------------------------------------------------- local function drawScene() canvas:clear(0x000000) for x = 1, SCREEN_W do local rayAngle = player.angle - FOV / 2 + FOV * (x - 1) / (SCREEN_W - 1) local dist, side, hitChar = castRay(player.x, player.y, rayAngle) local corrected = dist * math.cos(rayAngle - player.angle) if corrected < 0.05 then corrected = 0.05 end zbuffer[x] = corrected local lineHeight = math.floor(SCREEN_H / corrected) local drawStart = math.floor(-lineHeight / 2 + SCREEN_H / 2) local drawEnd = drawStart + lineHeight local clipStart = drawStart < 1 and 1 or drawStart local clipEnd = drawEnd > SCREEN_H and SCREEN_H or drawEnd local baseColor = WALL_COLORS[hitChar] or DEFAULT_WALL_COLOR if side == 1 then baseColor = shade(baseColor, 0.7) end local brightness = 1 - (corrected / MAX_DEPTH) * 0.85 local wallColor = shade(baseColor, brightness) if clipStart > 1 then canvas:fillRect(x, 1, 1, clipStart - 1, CEIL_COLOR) end if clipEnd >= clipStart then canvas:fillRect(x, clipStart, 1, clipEnd - clipStart + 1, wallColor) end if clipEnd < SCREEN_H then canvas:fillRect(x, clipEnd + 1, 1, SCREEN_H - clipEnd, FLOOR_COLOR) end end -- Спрайты врагов, дальние сначала local visible = {} for _, en in ipairs(enemies) do if en.alive then local dx, dy = en.x - player.x, en.y - player.y local dist = math.sqrt(dx * dx + dy * dy) table.insert(visible, { e = en, dist = dist, dx = dx, dy = dy }) end end table.sort(visible, function(a, b) return a.dist > b.dist end) for _, v in ipairs(visible) do local angleTo = math.atan(v.dy, v.dx) - player.angle while angleTo > math.pi do angleTo = angleTo - 2 * math.pi end while angleTo < -math.pi do angleTo = angleTo + 2 * math.pi end if math.abs(angleTo) < FOV / 2 + 0.3 and v.dist < MAX_DEPTH then local screenX = (0.5 + angleTo / FOV) * SCREEN_W local size = SCREEN_H / v.dist local half = size / 2 local top = math.floor(SCREEN_H / 2 - half) local bottom = math.floor(SCREEN_H / 2 + half) local left = math.floor(screenX - half * 0.5) local right = math.floor(screenX + half * 0.5) local brightness = 1 - (v.dist / MAX_DEPTH) * 0.8 local color = shade(ENEMY_COLOR, brightness) local clipTop = top < 1 and 1 or top local clipBottom = bottom > SCREEN_H and SCREEN_H or bottom for sx = left, right do if sx >= 1 and sx <= SCREEN_W and v.dist < zbuffer[sx] then canvas:fillRect(sx, clipTop, 1, clipBottom - clipTop + 1, color) end end end end -- прицел local cx, cy = math.floor(SCREEN_W / 2), math.floor(SCREEN_H / 2) canvas:fillRect(cx, cy - 3, 1, 7, 0xFFFFFF) canvas:fillRect(cx - 3, cy, 7, 1, 0xFFFFFF) canvas:present() end local function drawHud() gpu.setBackground(0x000000) gpu.setForeground(0xFFFFFF) local statusY = canvasRows + 1 gpu.fill(1, statusY, W_CHARS, HUD_ROWS, " ") gpu.set(2, statusY, string.format( "HP:%d Патроны:%d Счёт:%d Врагов осталось: %d", player.health, player.ammo, score, (function() local n = 0 for _, e in ipairs(enemies) do if e.alive then n = n + 1 end end return n end)() )) gpu.set(2, statusY + 1, "WASD-движение Стрелки-поворот Space-выстрел Esc-выход") if gameOverMsg then gpu.set(2, statusY + 2, gameOverMsg) end end ---------------------------------------------------------------------- -- Логика игрока/врагов ---------------------------------------------------------------------- local function tryMove(nx, ny) if not isWall(math.floor(nx + PLAYER_RADIUS * (nx > player.x and 1 or -1)), math.floor(player.y)) then player.x = nx end if not isWall(math.floor(player.x), math.floor(ny + PLAYER_RADIUS * (ny > player.y and 1 or -1))) then player.y = ny end end local function moveForward(sign) local nx = player.x + math.cos(player.angle) * MOVE_SPEED * sign local ny = player.y + math.sin(player.angle) * MOVE_SPEED * sign tryMove(nx, ny) end local function strafe(sign, dt) local a = player.angle + sign * math.pi / 2 local nx = player.x + math.cos(a) * MOVE_SPEED * dt local ny = player.y + math.sin(a) * MOVE_SPEED * dt tryMove(nx, ny) end local lastShot = -100 local function shoot() local now = computer.uptime() if now - lastShot < SHOOT_COOLDOWN then return end if player.ammo <= 0 then return end lastShot = now player.ammo = player.ammo - 1 local wallDist = select(1, castRay(player.x, player.y, player.angle)) local bestEnemy, bestDist for _, en in ipairs(enemies) do if en.alive then local dx, dy = en.x - player.x, en.y - player.y local dist = math.sqrt(dx * dx + dy * dy) if dist < SHOOT_RANGE and dist < wallDist then local angleTo = math.atan(dy, dx) - player.angle while angleTo > math.pi do angleTo = angleTo - 2 * math.pi end while angleTo < -math.pi do angleTo = angleTo + 2 * math.pi end if math.abs(angleTo) < SHOOT_TOLERANCE and (not bestDist or dist < bestDist) then bestEnemy, bestDist = en, dist end end end end if bestEnemy then bestEnemy.health = bestEnemy.health - 1 if bestEnemy.health <= 0 then bestEnemy.alive = false score = score + 1 end end end local lastDamage = -100 local function updateEnemies(dt) local now = computer.uptime() for _, en in ipairs(enemies) do if en.alive then local dx, dy = player.x - en.x, player.y - en.y local dist = math.sqrt(dx * dx + dy * dy) if dist > 0.01 and dist < 8 then local nx = en.x + (dx / dist) * en.speed * dt local ny = en.y + (dy / dist) * en.speed * dt if not isWall(math.floor(nx), math.floor(en.y)) then en.x = nx end if not isWall(math.floor(en.x), math.floor(ny)) then en.y = ny end end if dist < 0.6 and now - lastDamage > 0.6 then lastDamage = now player.health = player.health - 8 end end end end ---------------------------------------------------------------------- -- Ввод ---------------------------------------------------------------------- local pressed = {} local function pollEvents() while true do local e, _, _, code = event.pull(0) if e == nil then break end if e == "key_down" then pressed[code] = true if code == keys.space then shoot() end if code == keys.escape then running = false end elseif e == "key_up" then pressed[code] = nil end end end ---------------------------------------------------------------------- -- Главный цикл ---------------------------------------------------------------------- local lastTime = computer.uptime() local ok, err = pcall(function() while running do local now = computer.uptime() local dt = now - lastTime lastTime = now if dt > 0.1 then dt = 0.1 end -- защита от рывков после лагов pollEvents() if pressed[keys.w] then moveForward(dt) end if pressed[keys.s] then moveForward(-dt) end if pressed[keys.a] then strafe(-1, dt) end if pressed[keys.d] then strafe(1, dt) end if pressed[keys.left] then player.angle = player.angle - ROT_SPEED * dt end if pressed[keys.right] then player.angle = player.angle + ROT_SPEED * dt end updateEnemies(dt) if player.health <= 0 then gameOverMsg = "ВЫ ПОГИБЛИ. Esc — выход." player.health = 0 running = false end local aliveLeft = 0 for _, e in ipairs(enemies) do if e.alive then aliveLeft = aliveLeft + 1 end end if aliveLeft == 0 and not gameOverMsg then gameOverMsg = "ПОБЕДА! Все враги уничтожены. Esc — выход." end drawScene() drawHud() if not running then -- дать увидеть финальный экран и подождать Esc while true do local e2, _, _, code2 = event.pull(0.1) if e2 == "key_down" and code2 == keys.escape then break end end end end end) canvas:free() gpu.setBackground(0x000000) gpu.setForeground(0xFFFFFF) term.clear() if not ok then print("Ошибка: " .. tostring(err)) else print(string.format("Игра окончена. Счёт: %d", score)) end