🕹️ 游戏操作逻辑
这是一款基于掌控板的第一人称视角3D迷宫游戏,使用OLED屏幕显示,通过触摸键与按钮实现操作。以下是控制说明:
A键:左转
B键:右转
T触摸键:向前移动
H触摸键:后退
Y + O同时触摸:打开小地图
迷宫终点(闪烁边框):走到这里即可通关,屏幕显示“胜利”与通关用时
游戏启动时,触摸任意触摸键来“打乱种子”,从而生成不同的迷宫,增加可玩性。
💡 技术原理科普
1️⃣ 光线投射(Raycasting)原理
本游戏采用经典的光线投射算法,模拟第一人称视角。主要步骤:
玩家视角每帧投射出若干条“射线”,检测它们碰撞迷宫墙壁的位置。
根据射线距离决定墙的高度,距离越远,墙越矮,形成3D视觉错觉。
每条射线会根据计算间隔决定在屏幕上绘制的位置和高度。
和《DOOM》、《Wolfenstein 3D》等经典游戏使用的技术类似。
2️⃣ 迷宫生成算法:随机深度优先遍历(DFS)
迷宫使用深度优先搜索(DFS)+ Fisher-Yates 洗牌实现,每次生成都不同:
起点从 (1,1) 开始,只挖奇数坐标。
随机选择方向向前挖通,如果没路了就回退。
最终在右下角 (MAP_WIDTH-2, MAP_HEIGHT-2) 设置为终点(用值 4 表示)。
3️⃣ 小地图与视角同步
小地图实时显示玩家当前位置与迷宫结构:
黑色为通道,白色为墙。
玩家位置用小方块标记。
触摸 Y 和 O 键即可开关显示。
4️⃣ FPS 计算与游戏计时
每秒计算一次帧数(FPS),用于判断渲染性能。
从开始进入游戏后计时,通关后会显示用时。
from mpython import *
import math
import time
import random
# ------------ 使用触摸键值设置随机种子 -------------
def init_touch_seed():
oled.fill(0)
oled.DispChar("触摸任意触摸键", 23, 15)
oled.DispChar("打乱迷宫种子", 29, 31)
oled.show()
time.sleep(1)
time.sleep_ms(10)
vP = touchPad_P.read()
time.sleep_ms(10)
vY = touchPad_Y.read()
time.sleep_ms(10)
vT = touchPad_T.read()
time.sleep_ms(10)
vH = touchPad_H.read()
time.sleep_ms(10)
vO = touchPad_O.read()
time.sleep_ms(10)
vN = touchPad_N.read()
seed = (vP * vY + vT + vH * vO * vN) & 0x7FFFFFFF
random.seed(seed)
init_touch_seed()
# ------------ 地图与迷宫生成 -------------
MAP_WIDTH = 21
MAP_HEIGHT = 21
game_map_1d = [1] * (MAP_WIDTH * MAP_HEIGHT)
def map_at(x, y):
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
return game_map_1d[y * MAP_WIDTH + x]
return 1
def set_map(x, y, value):
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
game_map_1d[y * MAP_WIDTH + x] = value
def fisher_yates_shuffle(lst):
for i in range(len(lst) - 1, 0, -1):
j = random.randint(0, i)
lst[i], lst[j] = lst[j], lst[i]
def generate_maze():
for y in range(MAP_HEIGHT):
for x in range(MAP_WIDTH):
set_map(x, y, 1)
stack = [(1, 1)]
set_map(1, 1, 0)
while stack:
x, y = stack[-1]
dirs = [(0, -2), (0, 2), (-2, 0), (2, 0)]
fisher_yates_shuffle(dirs)
carved = False
for dx, dy in dirs:
nx, ny = x + dx, y + dy
if 0 < nx < MAP_WIDTH - 1 and 0 < ny < MAP_HEIGHT - 1 and map_at(nx, ny) == 1:
set_map(nx, ny, 0)
set_map(x + dx // 2, y + dy // 2, 0)
stack.append((nx, ny))
carved = True
break
if not carved:
stack.pop()
set_map(MAP_WIDTH - 2, MAP_HEIGHT - 2, 4)
generate_maze()
# ------------ 玩家与视角设置 -------------
player_x = 1.5
player_y = 1.5
player_angle = 0.0
SCREEN_WIDTH = 128
SCREEN_HEIGHT = 64
FOV = math.pi / 3
MAX_DEPTH = 8.0
MAX_RAY_COUNT = 48
ANGLE_STEPS = 192
angle_to_radians = [i * 2 * math.pi / ANGLE_STEPS for i in range(ANGLE_STEPS)]
cos_table = [math.cos(r) for r in angle_to_radians]
sin_table = [math.sin(r) for r in angle_to_radians]
def get_angle_index(angle_radians):
return int(angle_radians * ANGLE_STEPS / (2 * math.pi)) % ANGLE_STEPS
def get_line_spacing(distance):
spacing = int(distance) + 1
return min(spacing, 6)
last_time = time.ticks_ms()
last_fps_update = last_time
current_fps = 0
# 添加计时器
start_time = time.ticks_ms()
elapsed_seconds = 0
show_minimap = False
mini_map_width = MAP_WIDTH * 3
mini_map_height = MAP_HEIGHT * 3
tile_size = 3
mini_map_x = SCREEN_WIDTH - MAP_WIDTH * tile_size
mini_map_y = 0
def draw_minimap():
oled.fill_rect(mini_map_x, mini_map_y, mini_map_width, mini_map_height, 0)
for y in range(MAP_HEIGHT):
for x in range(MAP_WIDTH):
val = map_at(x, y)
px = mini_map_x + x * tile_size
py = mini_map_y + y * tile_size
if val == 1:
oled.fill_rect(px, py, tile_size, tile_size, 1)
elif val == 4:
oled.rect(px, py, tile_size, tile_size, 1)
mini_px = int(mini_map_x + player_x * tile_size)
mini_py = int(mini_map_y + player_y * tile_size)
oled.fill_rect(mini_px, mini_py, 2, 2, 1)
def clear_minimap():
oled.fill_rect(mini_map_x, mini_map_y, mini_map_width, mini_map_height, 0)
# ------------ 光线投射 -------------
def cast_rays(fps):
global elapsed_seconds
oled.fill(0)
x = 0
rays_drawn = 0
while x < SCREEN_WIDTH and rays_drawn < MAX_RAY_COUNT:
ray_angle = player_angle - FOV / 2 + (x / SCREEN_WIDTH) * FOV
ray_idx = get_angle_index(ray_angle)
ray_dir_x = cos_table[ray_idx]
ray_dir_y = sin_table[ray_idx]
map_x = int(player_x)
map_y = int(player_y)
delta_dist_x = abs(1 / ray_dir_x) if ray_dir_x != 0 else 1e30
delta_dist_y = abs(1 / ray_dir_y) if ray_dir_y != 0 else 1e30
if ray_dir_x < 0:
step_x = -1
side_dist_x = (player_x - map_x) * delta_dist_x
else:
step_x = 1
side_dist_x = (map_x + 1.0 - player_x) * delta_dist_x
if ray_dir_y < 0:
step_y = -1
side_dist_y = (player_y - map_y) * delta_dist_y
else:
step_y = 1
side_dist_y = (map_y + 1.0 - player_y) * delta_dist_y
hit = False
side = 0
while not hit:
if side_dist_x < side_dist_y:
side_dist_x += delta_dist_x
map_x += step_x
side = 0
else:
side_dist_y += delta_dist_y
map_y += step_y
side = 1
if map_x < 0 or map_x >= MAP_WIDTH or map_y < 0 or map_y >= MAP_HEIGHT:
hit = True
break
if map_at(map_x, map_y) in [1, 4]:
hit = True
if side == 0:
perp_wall_dist = (map_x - player_x + (1 - step_x) / 2) / ray_dir_x
else:
perp_wall_dist = (map_y - player_y + (1 - step_y) / 2) / ray_dir_y
angle_diff = ray_angle - player_angle
perp_wall_dist *= math.cos(angle_diff)
if perp_wall_dist < 0.1:
perp_wall_dist = 0.1
wall_height = int(SCREEN_HEIGHT / perp_wall_dist)
wall_height = min(wall_height, SCREEN_HEIGHT)
wall_start = max(0, int(SCREEN_HEIGHT / 2 - wall_height / 2))
wall_end = min(SCREEN_HEIGHT, int(SCREEN_HEIGHT / 2 + wall_height / 2))
oled.line(x, wall_start, x, wall_end, 1)
spacing = get_line_spacing(perp_wall_dist)
remaining_pixels = SCREEN_WIDTH - x
max_spacing = remaining_pixels // (MAX_RAY_COUNT - rays_drawn) if (MAX_RAY_COUNT - rays_drawn) > 0 else remaining_pixels
spacing = max(spacing, max_spacing)
x += spacing
rays_drawn += 1
oled.DispChar("FPS:%d" % fps, 0, 0)
elapsed_seconds = time.ticks_diff(time.ticks_ms(), start_time) // 1000
oled.DispChar("%ds" % elapsed_seconds, 40, 0)
if show_minimap:
draw_minimap()
oled.show()
# ------------ 游戏循环 -------------
def game_loop():
global player_x, player_y, player_angle, last_time, last_fps_update, current_fps, show_minimap
while True:
now = time.ticks_ms()
elapsed = time.ticks_diff(now, last_time)
last_time = now
if elapsed == 0:
elapsed = 1
if time.ticks_diff(now, last_fps_update) >= 1000:
current_fps = 1000 // elapsed
last_fps_update = now
fps = current_fps
if button_a.value() == 0:
player_angle -= 0.1
if button_b.value() == 0:
player_angle += 0.1
player_angle %= 2 * math.pi
if touchPad_T.read() < 450:
idx = get_angle_index(player_angle)
dx = cos_table[idx] * 0.12
dy = sin_table[idx] * 0.12
new_x = player_x + dx
new_y = player_y + dy
cell_value = map_at(int(new_x), int(new_y))
if cell_value == 0:
player_x = new_x
player_y = new_y
elif cell_value == 4:
oled.fill(0)
oled.DispChar("You Win!", 39, 15)
oled.DispChar("Time: %ds" % elapsed_seconds, 32, 31)
oled.show()
time.sleep(5)
break
if touchPad_H.read() < 450:
idx = get_angle_index(player_angle)
dx = cos_table[idx] * 0.12
dy = sin_table[idx] * 0.12
new_x = player_x - dx
new_y = player_y - dy
if map_at(int(new_x), int(new_y)) == 0:
player_x = new_x
player_y = new_y
current_minimap = (touchPad_Y.read() < 450 and touchPad_O.read() < 450)
if current_minimap and not show_minimap:
show_minimap = True
elif not current_minimap and show_minimap:
clear_minimap()
show_minimap = False
cast_rays(fps)
time.sleep(0.001)
# ------------ 启动游戏 -------------
game_loop()
如果你喜欢这款游戏或想要继续开发更多功能(比如多层地图、敌人、道具等),欢迎评论交流!
评论