回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

掌控板 3D迷宫游戏(第二版):光线投射实现第一人称视角! 简单

头像 dfrwh 2025.06.02 38 0

 

🕹️ 游戏操作逻辑

 

 

这是一款基于掌控板的第一人称视角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()

 

如果你喜欢这款游戏或想要继续开发更多功能(比如多层地图、敌人、道具等),欢迎评论交流!

评论

user-avatar