HarmonyOS 6游戏开发实战:基于悬浮导航与沉浸光感的“光影迷宫“解谜游戏

文章目录

    • 每日一句正能量
    • [一、前言:当游戏遇上HarmonyOS 6新特性](#一、前言:当游戏遇上HarmonyOS 6新特性)
    • 二、游戏架构设计
      • [2.1 技术选型](#2.1 技术选型)
      • [2.2 游戏核心机制](#2.2 游戏核心机制)
    • 三、核心代码实战
      • [3.1 游戏主场景与沉浸窗口配置](#3.1 游戏主场景与沉浸窗口配置)
      • [3.2 游戏核心渲染引擎(GameEngine.ets)](#3.2 游戏核心渲染引擎(GameEngine.ets))
      • [3.3 游戏悬浮控制盘(GameFloatNav.ets)](#3.3 游戏悬浮控制盘(GameFloatNav.ets))
      • [3.4 游戏主页面(GamePage.ets)](#3.4 游戏主页面(GamePage.ets))
      • [3.5 游戏光效同步控制器](#3.5 游戏光效同步控制器)
    • 四、关键技术总结
      • [4.1 游戏场景下的悬浮导航设计原则](#4.1 游戏场景下的悬浮导航设计原则)
      • [4.2 沉浸光感在游戏中的创新应用](#4.2 沉浸光感在游戏中的创新应用)
      • [4.3 性能优化策略](#4.3 性能优化策略)
    • 五、调试与发布建议
    • 六、结语

每日一句正能量

我始终相信努力奋斗的意义,因为我想给自己一个牛逼的机会,趁自己还年轻;因为我必须给自己一个交代。因为我就是那么一个老掉牙的人,我相信梦想,我相信温暖,我相信理想。我相信我的选择不会错,我相信我的梦想不会错,我相信遗憾比失败更可怕。早安!

一、前言:当游戏遇上HarmonyOS 6新特性

HarmonyOS 6(API 23)带来的**悬浮导航(Float Navigation)沉浸光感(Immersive Light Effects)**不仅是系统UI的革新,更为游戏开发提供了全新的交互与视觉维度。传统游戏中的固定UI会遮挡游戏画面,破坏沉浸感;而HarmonyOS 6的悬浮导航允许UI元素以半透明、可交互的方式悬浮于游戏场景之上,配合沉浸光感系统,能够实现游戏内光源与UI光效的完美联动。

本文将构建一款名为**"光影迷宫"**的2D解谜游戏,玩家通过控制光源在迷宫中探索,利用HarmonyOS 6的新特性实现:

  • 游戏内悬浮控制盘:半透明悬浮导航替代传统虚拟摇杆
  • 动态光效同步:玩家光源与UI光效、系统光效实时联动
  • 玻璃拟态游戏UI:设置面板、道具栏采用毛玻璃效果
  • P3广色域色彩:利用HarmonyOS 6的广色域支持呈现更绚丽的游戏画面

二、游戏架构设计

2.1 技术选型

模块 技术方案 HarmonyOS 6特性
游戏渲染 Canvas2D + ArkUI动画 P3广色域渲染
物理引擎 自定义轻量级碰撞检测 -
UI系统 悬浮导航组件 Float Navigation
光效系统 自定义着色器 + 系统光效 Immersive Light
状态管理 AppStorage + 自定义事件总线 跨组件通信

2.2 游戏核心机制

玩家控制一个发光球体在暗黑迷宫中探索:

  • 光源机制:球体发出动态光晕照亮周围,光强随时间衰减
  • 悬浮导航:屏幕边缘悬浮透明控制盘,不遮挡游戏画面
  • 光效联动:当玩家收集彩色水晶时,系统UI光效同步变化
  • 解谜元素:利用镜面反射、光色混合等机制解开机关

三、核心代码实战

3.1 游戏主场景与沉浸窗口配置

代码亮点 :通过setWindowLayoutFullScreenexpandSafeArea实现真正的全屏游戏,内容延伸至状态栏和导航栏,配合深色背景让光效更加突出。

typescript 复制代码
// entry/src/main/ets/ability/GameAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

export default class GameAbility extends UIAbility {
  async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
    windowStage.loadContent('pages/GamePage', async (err) => {
      if (err.code) {
        console.error('Failed to load game content:', JSON.stringify(err));
        return;
      }

      try {
        const mainWindow = windowStage.getMainWindowSync();
        
        // 1. 全屏布局,游戏内容延伸至所有边缘
        await mainWindow.setWindowLayoutFullScreen(true);
        
        // 2. 透明背景,允许游戏光效"穿透"到系统层
        await mainWindow.setWindowBackgroundColor('#00000000');
        
        // 3. 隐藏系统状态栏和导航栏,纯手势操作
        await mainWindow.setWindowSystemBarEnable({
          statusBar: false,
          navigationBar: false
        });
        
        // 4. 设置屏幕常亮,避免游戏过程中息屏
        await mainWindow.setWindowKeepScreenOn(true);
        
        // 5. 高帧率模式(90/120Hz)
        await mainWindow.setPreferredRefreshRate(120);
        
        console.info('Game window configured for immersive experience');
      } catch (error) {
        console.error('Window setup failed:', (error as BusinessError).message);
      }
    });
  }

  onWindowStageDestroy(): void {
    // 恢复屏幕自动息屏
    console.info('Game ability destroyed');
  }
}

3.2 游戏核心渲染引擎(GameEngine.ets)

代码亮点:基于Canvas2D构建轻量级游戏引擎,实现动态光源、粒子系统和碰撞检测,所有视觉元素均支持P3广色域色彩空间。

typescript 复制代码
// entry/src/main/ets/engine/GameEngine.ets
import { drawing } from '@kit.ArkGraphics2D';

// 游戏实体接口
interface GameEntity {
  x: number;
  y: number;
  width: number;
  height: number;
  color: string;
  update(deltaTime: number): void;
  render(ctx: CanvasRenderingContext2D): void;
}

// 光源配置
interface LightSource {
  x: number;
  y: number;
  radius: number;
  color: string;
  intensity: number;      // 0-1
  pulseSpeed: number;     // 呼吸速度
  flickerIntensity: number; // 闪烁强度
}

// 玩家实体
class Player implements GameEntity {
  x: number = 400;
  y: number = 300;
  width: number = 30;
  height: number = 30;
  color: string = '#00D2FF';
  velocityX: number = 0;
  velocityY: number = 0;
  speed: number = 200; // 像素/秒
  lightRadius: number = 150;
  lightIntensity: number = 1.0;
  
  // 光效动画参数
  private pulsePhase: number = 0;
  private flickerTimer: number = 0;

  update(deltaTime: number): void {
    // 更新位置
    this.x += this.velocityX * deltaTime;
    this.y += this.velocityY * deltaTime;
    
    // 边界约束
    this.x = Math.max(15, Math.min(785, this.x));
    this.y = Math.max(15, Math.min(585, this.y));
    
    // 光源呼吸动画
    this.pulsePhase += deltaTime * 2;
    const pulseFactor = Math.sin(this.pulsePhase) * 0.1 + 1;
    
    // 随机闪烁模拟真实光源
    this.flickerTimer += deltaTime;
    if (this.flickerTimer > 0.05) {
      this.flickerTimer = 0;
      this.lightIntensity = 0.9 + Math.random() * 0.2;
    }
    
    // 动态光半径
    this.lightRadius = 150 * pulseFactor * this.lightIntensity;
  }

  render(ctx: CanvasRenderingContext2D): void {
    // 绘制外发光(多层光晕)
    for (let i = 3; i > 0; i--) {
      const gradient = ctx.createRadialGradient(
        this.x, this.y, 0,
        this.x, this.y, this.lightRadius * (i * 0.4 + 0.5)
      );
      gradient.addColorStop(0, this.color + Math.floor(this.lightIntensity * 80).toString(16).padStart(2, '0'));
      gradient.addColorStop(1, 'transparent');
      
      ctx.fillStyle = gradient;
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.lightRadius * (i * 0.4 + 0.5), 0, Math.PI * 2);
      ctx.fill();
    }
    
    // 绘制实体核心(高亮)
    ctx.fillStyle = '#FFFFFF';
    ctx.shadowColor = this.color;
    ctx.shadowBlur = 20;
    ctx.beginPath();
    ctx.arc(this.x, this.y, 8, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
    
    // 绘制光晕核心
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, 12, 0, Math.PI * 2);
    ctx.fill();
  }
}

// 水晶收集物
class Crystal implements GameEntity {
  x: number;
  y: number;
  width: number = 24;
  height: number = 24;
  color: string;
  collected: boolean = false;
  private floatPhase: number = 0;
  private rotation: number = 0;

  constructor(x: number, y: number, color: string) {
    this.x = x;
    this.y = y;
    this.color = color;
  }

  update(deltaTime: number): void {
    if (this.collected) return;
    this.floatPhase += deltaTime * 3;
    this.rotation += deltaTime * 2;
  }

  render(ctx: CanvasRenderingContext2D): void {
    if (this.collected) return;
    
    const floatY = Math.sin(this.floatPhase) * 5;
    
    ctx.save();
    ctx.translate(this.x, this.y + floatY);
    ctx.rotate(this.rotation);
    
    // 水晶发光效果
    ctx.shadowColor = this.color;
    ctx.shadowBlur = 15;
    
    // 绘制菱形水晶
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.moveTo(0, -12);
    ctx.lineTo(10, 0);
    ctx.lineTo(0, 12);
    ctx.lineTo(-10, 0);
    ctx.closePath();
    ctx.fill();
    
    // 内部高光
    ctx.fillStyle = 'rgba(255,255,255,0.6)';
    ctx.beginPath();
    ctx.moveTo(0, -8);
    ctx.lineTo(5, 0);
    ctx.lineTo(0, 4);
    ctx.lineTo(-5, 0);
    ctx.closePath();
    ctx.fill();
    
    ctx.restore();
    ctx.shadowBlur = 0;
  }
}

// 墙壁(迷宫障碍物)
class Wall implements GameEntity {
  x: number;
  y: number;
  width: number;
  height: number;
  color: string = '#2A2A3E';

  constructor(x: number, y: number, w: number, h: number) {
    this.x = x;
    this.y = y;
    this.width = w;
    this.height = h;
  }

  update(_deltaTime: number): void {}

  render(ctx: CanvasRenderingContext2D): void {
    // 墙壁基础
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
    
    // 边缘高光(模拟环境光反射)
    ctx.strokeStyle = 'rgba(255,255,255,0.05)';
    ctx.lineWidth = 1;
    ctx.strokeRect(this.x, this.y, this.width, this.height);
  }
}

// 粒子系统(用于收集特效)
class ParticleSystem {
  particles: Array<{
    x: number; y: number;
    vx: number; vy: number;
    life: number; maxLife: number;
    color: string; size: number;
  }> = [];

  emit(x: number, y: number, color: string, count: number = 10): void {
    for (let i = 0; i < count; i++) {
      const angle = (Math.PI * 2 * i) / count;
      const speed = 50 + Math.random() * 100;
      this.particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed,
        life: 1.0,
        maxLife: 0.5 + Math.random() * 0.5,
        color,
        size: 3 + Math.random() * 4
      });
    }
  }

  update(deltaTime: number): void {
    this.particles = this.particles.filter(p => {
      p.x += p.vx * deltaTime;
      p.y += p.vy * deltaTime;
      p.life -= deltaTime / p.maxLife;
      p.vx *= 0.98; // 阻力
      p.vy *= 0.98;
      return p.life > 0;
    });
  }

  render(ctx: CanvasRenderingContext2D): void {
    this.particles.forEach(p => {
      ctx.globalAlpha = p.life;
      ctx.fillStyle = p.color;
      ctx.shadowColor = p.color;
      ctx.shadowBlur = 10;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
      ctx.fill();
    });
    ctx.globalAlpha = 1;
    ctx.shadowBlur = 0;
  }
}

// 游戏引擎主类
export class GameEngine {
  private canvas: CanvasRenderingContext2D | null = null;
  private lastTime: number = 0;
  private isRunning: boolean = false;
  
  // 游戏实体
  player: Player = new Player();
  walls: Wall[] = [];
  crystals: Crystal[] = [];
  particles: ParticleSystem = new ParticleSystem();
  
  // 游戏状态
  score: number = 0;
  gameTime: number = 0;
  collectedColors: Set<string> = new Set();
  
  // 光效同步回调
  onLightEffectChange?: (color: string, intensity: number) => void;

  initialize(canvasContext: CanvasRenderingContext2D): void {
    this.canvas = canvasContext;
    this.setupLevel();
  }

  private setupLevel(): void {
    // 初始化迷宫墙壁
    this.walls = [
      new Wall(100, 100, 200, 20),
      new Wall(400, 50, 20, 200),
      new Wall(600, 300, 150, 20),
      new Wall(200, 400, 20, 150),
      new Wall(500, 500, 200, 20),
      // 边界墙
      new Wall(0, 0, 800, 10),
      new Wall(0, 590, 800, 10),
      new Wall(0, 0, 10, 600),
      new Wall(790, 0, 10, 600)
    ];
    
    // 初始化水晶(不同颜色触发不同光效)
    this.crystals = [
      new Crystal(150, 200, '#FF6B6B'),  // 红色 - 热情
      new Crystal(650, 150, '#4ECDC4'),  // 青色 - 宁静
      new Crystal(300, 450, '#FFE66D'),  // 黄色 - 活力
      new Crystal(700, 500, '#95E1D3'),  // 薄荷 - 清新
      new Crystal(450, 300, '#F38181')   // 粉红 - 浪漫
    ];
  }

  start(): void {
    this.isRunning = true;
    this.lastTime = Date.now();
    this.gameLoop();
  }

  stop(): void {
    this.isRunning = false;
  }

  private gameLoop(): void {
    if (!this.isRunning || !this.canvas) return;
    
    const currentTime = Date.now();
    const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05); // 限制最大帧间隔
    this.lastTime = currentTime;
    
    this.update(deltaTime);
    this.render();
    
    requestAnimationFrame(() => this.gameLoop());
  }

  private update(deltaTime: number): void {
    this.gameTime += deltaTime;
    
    // 更新玩家
    this.player.update(deltaTime);
    
    // 更新水晶
    this.crystals.forEach(crystal => crystal.update(deltaTime));
    
    // 碰撞检测:玩家 vs 墙壁
    this.handleWallCollision();
    
    // 碰撞检测:玩家 vs 水晶
    this.handleCrystalCollection();
    
    // 更新粒子
    this.particles.update(deltaTime);
  }

  private handleWallCollision(): void {
    const playerRect = {
      left: this.player.x - this.player.width / 2,
      right: this.player.x + this.player.width / 2,
      top: this.player.y - this.player.height / 2,
      bottom: this.player.y + this.player.height / 2
    };
    
    this.walls.forEach(wall => {
      if (this.isRectColliding(playerRect, wall)) {
        // 简单回弹
        const overlapX = Math.min(playerRect.right - wall.x, wall.x + wall.width - playerRect.left);
        const overlapY = Math.min(playerRect.bottom - wall.y, wall.y + wall.height - playerRect.top);
        
        if (overlapX < overlapY) {
          this.player.x = this.player.x < wall.x + wall.width / 2 ? 
            wall.x - this.player.width / 2 : wall.x + wall.width + this.player.width / 2;
        } else {
          this.player.y = this.player.y < wall.y + wall.height / 2 ? 
            wall.y - this.player.height / 2 : wall.y + wall.height + this.player.height / 2;
        }
        
        this.player.velocityX = 0;
        this.player.velocityY = 0;
      }
    });
  }

  private handleCrystalCollection(): void {
    this.crystals.forEach(crystal => {
      if (crystal.collected) return;
      
      const dx = this.player.x - crystal.x;
      const dy = this.player.y - crystal.y;
      const distance = Math.sqrt(dx * dx + dy * dy);
      
      if (distance < this.player.lightRadius * 0.3 + 20) {
        // 收集水晶
        crystal.collected = true;
        this.score += 100;
        this.collectedColors.add(crystal.color);
        
        // 发射粒子特效
        this.particles.emit(crystal.x, crystal.y, crystal.color, 15);
        
        // 同步系统光效(关键:HarmonyOS 6特性)
        this.syncSystemLightEffect(crystal.color);
        
        // 临时改变玩家光源颜色
        this.player.color = crystal.color;
      }
    });
  }

  private syncSystemLightEffect(color: string): void {
    // 通知UI层更新光效
    if (this.onLightEffectChange) {
      this.onLightEffectChange(color, 0.8);
    }
    
    // 通过AppStorage同步到悬浮导航组件
    AppStorage.setOrCreate('game_light_color', color);
    AppStorage.setOrCreate('game_light_pulse', Date.now());
  }

  private isRectColliding(rect: any, wall: Wall): boolean {
    return rect.left < wall.x + wall.width &&
           rect.right > wall.x &&
           rect.top < wall.y + wall.height &&
           rect.bottom > wall.y;
  }

  private render(): void {
    if (!this.canvas) return;
    const ctx = this.canvas;
    
    // 清空画布(深色背景增强光效)
    ctx.fillStyle = '#0a0a0f';
    ctx.fillRect(0, 0, 800, 600);
    
    // 绘制环境雾(未照亮区域更暗)
    ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
    ctx.fillRect(0, 0, 800, 600);
    
    // 使用globalCompositeOperation实现光照效果
    ctx.globalCompositeOperation = 'destination-out';
    
    // 绘制玩家光源照亮区域
    const lightGradient = ctx.createRadialGradient(
      this.player.x, this.player.y, 0,
      this.player.x, this.player.y, this.player.lightRadius
    );
    lightGradient.addColorStop(0, `rgba(255, 255, 255, ${this.player.lightIntensity})`);
    lightGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
    
    ctx.fillStyle = lightGradient;
    ctx.beginPath();
    ctx.arc(this.player.x, this.player.y, this.player.lightRadius, 0, Math.PI * 2);
    ctx.fill();
    
    // 恢复混合模式
    ctx.globalCompositeOperation = 'source-over';
    
    // 绘制墙壁
    this.walls.forEach(wall => wall.render(ctx));
    
    // 绘制水晶
    this.crystals.forEach(crystal => crystal.render(ctx));
    
    // 绘制粒子
    this.particles.render(ctx);
    
    // 绘制玩家
    this.player.render(ctx);
  }

  // 输入控制
  setPlayerVelocity(vx: number, vy: number): void {
    this.player.velocityX = vx * this.player.speed;
    this.player.velocityY = vy * this.player.speed;
  }
}

3.3 游戏悬浮控制盘(GameFloatNav.ets)

代码亮点:针对游戏场景优化的悬浮导航组件,采用扇形/环形布局替代传统线性导航,支持手势拖拽定位,确保不遮挡游戏核心画面区域。

typescript 复制代码
// entry/src/main/ets/components/GameFloatNav.ets
import { window } from '@kit.ArkUI';

// 控制盘配置
interface ControlConfig {
  position: 'left' | 'right' | 'bottom';  // 悬浮位置
  autoHide: boolean;                       // 是否自动隐藏
  transparency: number;                    // 透明度
  size: number;                            // 控制盘大小
}

// 方向键状态
interface DirectionState {
  up: boolean;
  down: boolean;
  left: boolean;
  right: boolean;
}

@Component
export struct GameFloatNav {
  // 回调函数
  onDirectionChange?: (dx: number, dy: number) => void;
  onActionPress?: (action: string) => void;
  
  @State config: ControlConfig = {
    position: 'left',
    autoHide: true,
    transparency: 0.6,
    size: 180
  };
  
  @State isExpanded: boolean = false;
  @State isDragging: boolean = false;
  @State navPosition: Position = { x: 20, y: 300 };
  @State activeDirections: DirectionState = {
    up: false, down: false, left: false, right: false
  };
  
  // 光效状态(与游戏同步)
  @State lightColor: string = '#00D2FF';
  @State lightIntensity: number = 0.6;
  
  // 自动隐藏计时器
  private hideTimer: number = -1;
  private touchStartPos: Position = { x: 0, y: 0 };

  aboutToAppear(): void {
    // 监听游戏光效变化
    AppStorage.watch('game_light_color', (color: string) => {
      this.lightColor = color;
      this.triggerPulse();
    });
  }

  private triggerPulse(): void {
    // 光效脉冲动画
    this.lightIntensity = 1.0;
    setTimeout(() => {
      this.lightIntensity = 0.6;
    }, 300);
  }

  private startAutoHide(): void {
    if (!this.config.autoHide) return;
    clearTimeout(this.hideTimer);
    this.hideTimer = setTimeout(() => {
      this.isExpanded = false;
    }, 3000);
  }

  private calculateDirection(): void {
    let dx = 0, dy = 0;
    if (this.activeDirections.up) dy -= 1;
    if (this.activeDirections.down) dy += 1;
    if (this.activeDirections.left) dx -= 1;
    if (this.activeDirections.right) dx += 1;
    
    // 归一化
    const length = Math.sqrt(dx * dx + dy * dy);
    if (length > 0) {
      dx /= length;
      dy /= length;
    }
    
    this.onDirectionChange?.(dx, dy);
  }

  build() {
    Stack() {
      // 背景光效层
      Column()
        .width(this.config.size)
        .height(this.config.size)
        .backgroundColor(this.lightColor)
        .blur(60)
        .opacity(this.lightIntensity * 0.3)
        .borderRadius(this.config.size / 2)
        .animation({
          duration: 300,
          curve: Curve.EaseInOut
        })

      // 主控制盘(玻璃拟态)
      Column() {
        // 方向控制区(十字键)
        this.buildDirectionPad()
        
        // 展开后的功能按钮
        if (this.isExpanded) {
          this.buildActionButtons()
        }
      }
      .width(this.isExpanded ? this.config.size : 80)
      .height(this.isExpanded ? this.config.size + 100 : 80)
      .backgroundColor(`rgba(30, 30, 40, ${this.config.transparency})`)
      .backdropFilter($r('sys.blur.20'))
      .borderRadius(this.isExpanded ? 24 : 40)
      .border({
        width: 1,
        color: `rgba(255, 255, 255, 0.1)`,
        style: BorderStyle.Solid
      })
      .shadow({
        radius: 20,
        color: 'rgba(0, 0, 0, 0.4)',
        offsetX: 0,
        offsetY: 4
      })
      .animation({
        duration: 250,
        curve: Curve.Spring,
        iterations: 1
      })

      // 拖拽指示器
      if (this.isDragging) {
        Column()
          .width(40)
          .height(40)
          .backgroundColor('rgba(255, 255, 255, 0.2)')
          .borderRadius(20)
          .position({ x: -20, y: -20 })
      }
    }
    .width(this.isExpanded ? this.config.size : 80)
    .height(this.isExpanded ? this.config.size + 100 : 80)
    .position({
      x: this.navPosition.x,
      y: this.navPosition.y
    })
    .onTouch((event: TouchEvent) => {
      switch (event.type) {
        case TouchType.Down:
          this.touchStartPos = { x: event.touches[0].x, y: event.touches[0].y };
          this.isDragging = true;
          break;
        case TouchType.Move:
          if (this.isDragging) {
            this.navPosition = {
              x: this.navPosition.x + event.touches[0].x - this.touchStartPos.x,
              y: this.navPosition.y + event.touches[0].y - this.touchStartPos.y
            };
          }
          break;
        case TouchType.Up:
          this.isDragging = false;
          this.isExpanded = !this.isExpanded;
          this.startAutoHide();
          break;
      }
    })
  }

  @Builder
  buildDirectionPad(): void {
    Column({ space: 4 }) {
      // 上
      this.buildDirectionButton('up', $r('app.media.ic_up'), 0, -1)
      
      Row({ space: 4 }) {
        // 左
        this.buildDirectionButton('left', $r('app.media.ic_left'), -1, 0)
        
        // 中心(功能按钮)
        Button() {
          Image($r('app.media.ic_menu'))
            .width(24)
            .height(24)
            .fillColor('#FFFFFF')
        }
        .type(ButtonType.Circle)
        .backgroundColor(this.lightColor)
        .width(50)
        .height(50)
        .shadow({
          radius: 10,
          color: this.lightColor,
          offsetX: 0,
          offsetY: 0
        })
        .onClick(() => {
          this.isExpanded = !this.isExpanded;
          this.startAutoHide();
        })

        // 右
        this.buildDirectionButton('right', $r('app.media.ic_right'), 1, 0)
      }

      // 下
      this.buildDirectionButton('down', $r('app.media.ic_down'), 0, 1)
    }
    .padding(8)
  }

  @Builder
  buildDirectionButton(direction: string, icon: Resource, dx: number, dy: number): void {
    Button() {
      Image(icon)
        .width(20)
        .height(20)
        .fillColor(this.activeDirections[direction as keyof DirectionState] ? this.lightColor : '#B0B0B0')
    }
    .type(ButtonType.Circle)
    .backgroundColor(
      this.activeDirections[direction as keyof DirectionState] ? 
      `rgba(255, 255, 255, 0.15)` : 'rgba(255, 255, 255, 0.05)'
    )
    .width(44)
    .height(44)
    .onTouch((event: TouchEvent) => {
      if (event.type === TouchType.Down) {
        this.activeDirections[direction as keyof DirectionState] = true;
      } else if (event.type === TouchType.Up) {
        this.activeDirections[direction as keyof DirectionState] = false;
      }
      this.calculateDirection();
    })
  }

  @Builder
  buildActionButtons(): void {
    Row({ space: 12 }) {
      // 道具按钮
      this.buildActionButton('item', $r('app.media.ic_item'), '使用道具')
      
      // 地图按钮
      this.buildActionButton('map', $r('app.media.ic_map'), '地图')
      
      // 设置按钮
      this.buildActionButton('settings', $r('app.media.ic_settings'), '设置')
    }
    .margin({ top: 16 })
    .padding({ left: 12, right: 12 })
  }

  @Builder
  buildActionButton(action: string, icon: Resource, label: string): void {
    Column() {
      Button() {
        Image(icon)
          .width(22)
          .height(22)
          .fillColor('#FFFFFF')
      }
      .type(ButtonType.Circle)
      .backgroundColor('rgba(255, 255, 255, 0.1)')
      .width(48)
      .height(48)
      .onClick(() => {
        this.onActionPress?.(action);
      })

      Text(label)
        .fontSize(10)
        .fontColor('rgba(255, 255, 255, 0.6)')
        .margin({ top: 4 })
    }
  }
}

游戏悬浮导航设计要点

  1. 环形布局:方向键采用十字布局,中心为菜单按钮,符合游戏手柄直觉
  2. 拖拽定位:玩家可自由拖动控制盘到任意位置,避免遮挡关键游戏区域
  3. 光效同步:收集水晶时控制盘背景光效同步变化,提供视觉反馈
  4. 自动隐藏:非操作状态下自动收缩为圆形按钮,最大化游戏视野

3.4 游戏主页面(GamePage.ets)

typescript 复制代码
// entry/src/main/ets/pages/GamePage.ets
import { GameEngine } from '../engine/GameEngine';
import { GameFloatNav } from '../components/GameFloatNav';
import { window } from '@kit.ArkUI';

@Entry
@Component
struct GamePage {
  private canvasContext: CanvasRenderingContext2D | null = null;
  private gameEngine: GameEngine = new GameEngine();
  
  @State score: number = 0;
  @State gameTime: string = '00:00';
  @State collectedCount: number = 0;
  @State totalCrystals: number = 5;
  @State showSettings: boolean = false;
  @State showMap: boolean = false;
  
  // 屏幕尺寸适配
  @State screenWidth: number = 800;
  @State screenHeight: number = 600;

  aboutToAppear(): void {
    // 获取屏幕尺寸
    this.getScreenSize();
  }

  private async getScreenSize(): Promise<void> {
    try {
      const win = await window.getLastWindow();
      const rect = win.getWindowProperties().windowRect;
      this.screenWidth = rect.width;
      this.screenHeight = rect.height;
    } catch (error) {
      console.error('Failed to get screen size:', error);
    }
  }

  build() {
    Stack({ alignContent: Alignment.Center }) {
      // 游戏画布
      Canvas(this.canvasContext)
        .width(this.screenWidth)
        .height(this.screenHeight)
        .backgroundColor('#0a0a0f')
        .onReady((context: CanvasRenderingContext2D) => {
          this.canvasContext = context;
          this.gameEngine.initialize(context);
          
          // 绑定光效同步回调
          this.gameEngine.onLightEffectChange = (color: string, intensity: number) => {
            this.syncSystemLightEffect(color, intensity);
          };
          
          this.gameEngine.start();
        })

      // 顶部HUD(玻璃拟态)
      this.buildHUD()

      // 悬浮控制盘
      GameFloatNav({
        onDirectionChange: (dx: number, dy: number) => {
          this.gameEngine.setPlayerVelocity(dx, dy);
        },
        onActionPress: (action: string) => {
          this.handleAction(action);
        }
      })

      // 设置面板(沉浸光感)
      if (this.showSettings) {
        SettingsPanel({
          onClose: () => { this.showSettings = false; }
        })
      }

      // 小地图(玻璃拟态)
      if (this.showMap) {
        MiniMap({
          playerX: this.gameEngine.player.x,
          playerY: this.gameEngine.player.y,
          walls: this.gameEngine.walls,
          crystals: this.gameEngine.crystals,
          onClose: () => { this.showMap = false; }
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
  }

  @Builder
  buildHUD(): void {
    Row() {
      // 分数
      Column() {
        Text(`${this.score}`)
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        
        Text('SCORE')
          .fontSize(10)
          .fontColor('rgba(255,255,255,0.5)')
      }
      .backgroundColor('rgba(30, 30, 40, 0.6)')
      .backdropFilter($r('sys.blur.15'))
      .padding({ left: 20, right: 20, top: 12, bottom: 12 })
      .borderRadius(16)

      // 收集进度
      Row({ space: 8 }) {
        ForEach([0, 1, 2, 3, 4], (index: number) => {
          Column()
            .width(24)
            .height(24)
            .backgroundColor(index < this.collectedCount ? '#4ECDC4' : 'rgba(255,255,255,0.1)')
            .borderRadius(12)
            .border({
              width: 2,
              color: index < this.collectedCount ? '#4ECDC4' : 'rgba(255,255,255,0.2)'
            })
        })
      }
      .backgroundColor('rgba(30, 30, 40, 0.6)')
      .backdropFilter($r('sys.blur.15'))
      .padding(12)
      .borderRadius(16)

      // 时间
      Column() {
        Text(this.gameTime)
          .fontSize(24)
          .fontColor('#FFFFFF')
          .fontVariant(FontVariant.TabNums)
      }
      .backgroundColor('rgba(30, 30, 40, 0.6)')
      .backdropFilter($r('sys.blur.15'))
      .padding({ left: 20, right: 20, top: 12, bottom: 12 })
      .borderRadius(16)
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceEvenly)
    .padding({ top: 40, left: 20, right: 20 })
    .position({ x: 0, y: 0 })
  }

  private handleAction(action: string): void {
    switch (action) {
      case 'settings':
        this.showSettings = true;
        break;
      case 'map':
        this.showMap = true;
        break;
      case 'item':
        // 使用道具逻辑
        break;
    }
  }

  private syncSystemLightEffect(color: string, intensity: number): void {
    // 同步到系统沉浸光效
    console.info(`Syncing light effect: ${color} at ${intensity}`);
    
    // 这里可以调用系统级光效API(HarmonyOS 6特性)
    // 例如:控制设备LED灯效、通知栏光效等
  }
}

// 设置面板组件(玻璃拟态沉浸光感)
@Component
struct SettingsPanel {
  onClose: () => void = () => {};
  
  @State musicVolume: number = 80;
  @State sfxVolume: number = 100;
  @State lightIntensity: number = 0.6;
  @State showFPS: boolean = false;

  build() {
    Stack() {
      // 背景遮罩
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('rgba(0, 0, 0, 0.7)')
        .onClick(() => this.onClose())

      // 设置面板(玻璃拟态)
      Column({ space: 20 }) {
        Text('游戏设置')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .margin({ bottom: 20 })

        // 音乐音量
        SettingSlider({
          label: '音乐音量',
          value: this.musicVolume,
          icon: $r('app.media.ic_music')
        })

        // 音效音量
        SettingSlider({
          label: '音效音量',
          value: this.sfxVolume,
          icon: $r('app.media.ic_sound')
        })

        // 光效强度
        SettingSlider({
          label: '光效强度',
          value: this.lightIntensity * 100,
          icon: $r('app.media.ic_light')
        })

        // FPS显示开关
        Row() {
          Text('显示FPS')
            .fontSize(16)
            .fontColor('#FFFFFF')
          
          Toggle({ type: ToggleType.Switch, isOn: this.showFPS })
            .selectedColor('#4ECDC4')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        // 关闭按钮
        Button('关闭')
          .width('100%')
          .height(48)
          .backgroundColor('rgba(255, 255, 255, 0.1)')
          .fontColor('#FFFFFF')
          .borderRadius(12)
          .onClick(() => this.onClose())
      }
      .width(320)
      .padding(24)
      .backgroundColor('rgba(30, 30, 40, 0.85)')
      .backdropFilter($r('sys.blur.30'))
      .borderRadius(24)
      .border({
        width: 1,
        color: 'rgba(255, 255, 255, 0.1)'
      })
      .shadow({
        radius: 30,
        color: 'rgba(0, 0, 0, 0.5)',
        offsetX: 0,
        offsetY: 10
      })
    }
    .width('100%')
    .height('100%')
  }
}

// 设置滑块组件
@Component
struct SettingSlider {
  label: string = '';
  @State value: number = 50;
  icon: Resource = $r('app.media.ic_settings');

  build() {
    Column({ space: 8 }) {
      Row({ space: 12 }) {
        Image(this.icon)
          .width(20)
          .height(20)
          .fillColor('#B0B0B0')
        
        Text(this.label)
          .fontSize(14)
          .fontColor('#FFFFFF')
        
        Text(`${Math.round(this.value)}%`)
          .fontSize(14)
          .fontColor('#4ECDC4')
          .layoutWeight(1)
          .textAlign(TextAlign.End)
      }
      .width('100%')

      Slider({
        value: this.value,
        min: 0,
        max: 100,
        step: 1,
        style: SliderStyle.OutSet
      })
        .width('100%')
        .selectedColor('#4ECDC4')
        .blockColor('#FFFFFF')
        .trackColor('rgba(255, 255, 255, 0.1)')
    }
    .width('100%')
  }
}

// 小地图组件
@Component
struct MiniMap {
  playerX: number = 0;
  playerY: number = 0;
  walls: Array<{x: number, y: number, width: number, height: number}> = [];
  crystals: Array<{x: number, y: number, collected: boolean}> = [];
  onClose: () => void = () => {};

  build() {
    Stack() {
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('rgba(0, 0, 0, 0.7)')
        .onClick(() => this.onClose())

      Column() {
        Text('迷宫地图')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .margin({ bottom: 16 })

        // 地图画布
        Canvas()
          .width(280)
          .height(210)
          .backgroundColor('rgba(20, 20, 30, 0.8)')
          .borderRadius(12)
          .onReady((ctx: CanvasRenderingContext2D) => {
            const scaleX = 280 / 800;
            const scaleY = 210 / 600;
            
            // 绘制墙壁
            ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
            this.walls.forEach(wall => {
              ctx.fillRect(
                wall.x * scaleX, 
                wall.y * scaleY, 
                wall.width * scaleX, 
                wall.height * scaleY
              );
            });
            
            // 绘制水晶
            this.crystals.forEach(crystal => {
              if (!crystal.collected) {
                ctx.fillStyle = '#4ECDC4';
                ctx.beginPath();
                ctx.arc(
                  crystal.x * scaleX, 
                  crystal.y * scaleY, 
                  4, 0, Math.PI * 2
                );
                ctx.fill();
              }
            });
            
            // 绘制玩家位置
            ctx.fillStyle = '#00D2FF';
            ctx.shadowColor = '#00D2FF';
            ctx.shadowBlur = 10;
            ctx.beginPath();
            ctx.arc(
              this.playerX * scaleX, 
              this.playerY * scaleY, 
              6, 0, Math.PI * 2
            );
            ctx.fill();
            ctx.shadowBlur = 0;
          })

        Button('关闭')
          .width('100%')
          .height(40)
          .backgroundColor('rgba(255, 255, 255, 0.1)')
          .fontColor('#FFFFFF')
          .borderRadius(8)
          .margin({ top: 16 })
          .onClick(() => this.onClose())
      }
      .width(320)
      .padding(20)
      .backgroundColor('rgba(30, 30, 40, 0.9)')
      .backdropFilter($r('sys.blur.30'))
      .borderRadius(20)
    }
    .width('100%')
    .height('100%')
  }
}

3.5 游戏光效同步控制器

typescript 复制代码
// entry/src/main/ets/utils/LightEffectController.ets
import { sensor } from '@kit.SensorServiceKit';

export class LightEffectController {
  private static instance: LightEffectController;
  private vibrator: sensor.Vibrator | null = null;
  
  // 当前光效状态
  currentColor: string = '#00D2FF';
  currentIntensity: number = 0.6;
  
  static getInstance(): LightEffectController {
    if (!LightEffectController.instance) {
      LightEffectController.instance = new LightEffectController();
    }
    return LightEffectController.instance;
  }

  async initialize(): Promise<void> {
    try {
      // 初始化震动反馈
      this.vibrator = sensor.vibrator;
      console.info('LightEffectController initialized');
    } catch (error) {
      console.error('Failed to initialize vibrator:', error);
    }
  }

  // 触发收集光效(带震动反馈)
  async triggerCollectionEffect(color: string): Promise<void> {
    this.currentColor = color;
    this.currentIntensity = 1.0;
    
    // 震动反馈
    try {
      await this.vibrator?.startVibration({
        type: 'time',
        duration: 100
      }, {
        id: 0,
        usage: 'game'
      });
    } catch (error) {
      console.error('Vibration failed:', error);
    }
    
    // 光效衰减
    this.fadeLightEffect();
  }

  private fadeLightEffect(): void {
    const fadeInterval = setInterval(() => {
      this.currentIntensity -= 0.05;
      if (this.currentIntensity <= 0.6) {
        this.currentIntensity = 0.6;
        clearInterval(fadeInterval);
      }
      
      // 同步到系统
      this.syncToSystem();
    }, 50);
  }

  private syncToSystem(): void {
    // 同步到AppStorage,触发UI更新
    AppStorage.setOrCreate('system_light_color', this.currentColor);
    AppStorage.setOrCreate('system_light_intensity', this.currentIntensity);
    
    // 这里可以调用HarmonyOS 6系统级光效API
    // 例如:控制设备氛围灯、屏幕边缘光效等
  }

  // 根据游戏时间调整环境光(昼夜循环)
  updateAmbientLight(gameTime: number): void {
    const cycle = (gameTime % 60) / 60; // 60秒一个周期
    const intensity = 0.3 + Math.sin(cycle * Math.PI * 2) * 0.3;
    
    AppStorage.setOrCreate('ambient_light_intensity', intensity);
  }
}

四、关键技术总结

4.1 游戏场景下的悬浮导航设计原则

设计维度 传统移动端游戏 HarmonyOS 6悬浮导航
位置固定性 固定于屏幕角落 可拖拽悬浮,自由定位
视觉遮挡 opaque背景遮挡画面 毛玻璃半透明,保持沉浸
交互反馈 简单点击 光效同步+震动反馈
状态变化 静态UI 动态光效随游戏状态变化
多任务 单窗口全屏 支持悬浮窗多任务

4.2 沉浸光感在游戏中的创新应用

  1. 情绪光效同步:玩家生命值低时UI泛红光,收集道具时闪烁对应颜色
  2. 环境光联动:游戏内昼夜变化同步调整系统屏幕色温
  3. 社交光效:多人游戏时不同玩家颜色通过光效区分
  4. 成就光效:解锁成就时触发全屏光效脉冲

4.3 性能优化策略

typescript 复制代码
// 光效性能优化:离屏渲染缓存
class LightEffectCache {
  private cache: Map<string, ImageBitmap> = new Map();
  
  getLightGradient(radius: number, color: string, intensity: number): ImageBitmap {
    const key = `${radius}-${color}-${intensity}`;
    if (!this.cache.has(key)) {
      // 创建离屏画布预渲染光效
      const offscreen = new OffscreenCanvas(radius * 2, radius * 2);
      const ctx = offscreen.getContext('2d')!;
      
      const gradient = ctx.createRadialGradient(
        radius, radius, 0,
        radius, radius, radius
      );
      gradient.addColorStop(0, color);
      gradient.addColorStop(1, 'transparent');
      
      ctx.fillStyle = gradient;
      ctx.globalAlpha = intensity;
      ctx.fillRect(0, 0, radius * 2, radius * 2);
      
      this.cache.set(key, offscreen.transferToImageBitmap());
    }
    return this.cache.get(key)!;
  }
}

五、调试与发布建议

  1. 真机性能测试:Canvas2D在HarmonyOS 6设备上的性能表现需通过真机验证,特别关注120Hz高刷模式下的帧率稳定性
  2. 光效适配:不同设备(手机/平板/PC)的光效强度应可配置,避免OLED屏幕烧屏
  3. 耗电优化:游戏光效+高刷会显著增加耗电,建议提供"省电模式"降低光效复杂度
  4. 无障碍支持:为视障玩家提供高对比度模式,关闭动态光效

六、结语

HarmonyOS 6的悬浮导航与沉浸光感特性为游戏开发开辟了全新的交互维度。通过"光影迷宫"的实战案例,我们展示了如何:

  • 构建不遮挡游戏画面的悬浮控制盘,支持自由拖拽定位
  • 实现游戏内光源与系统UI光效的实时同步联动
  • 利用玻璃拟态效果打造现代化的游戏UI面板
  • 通过P3广色域支持呈现更绚丽的游戏色彩

这些技术不仅提升了游戏的视觉品质,更创造了操作系统与游戏内容深度融合的沉浸式体验。期待HarmonyOS游戏开发者们能够利用这些新特性,创造出更多令人惊叹的交互体验。


转载自:https://blog.csdn.net/u014727709/article/details/160269775

欢迎 👍点赞✍评论⭐收藏,欢迎指正

相关推荐
棋子入局2 小时前
C语言制作消消乐游戏
游戏
liulian09163 小时前
Flutter 三方库 connectivity_plus 的鸿蒙化适配与网络状态管理实战
网络·flutter·华为·学习方法·harmonyos
源码老李3 小时前
Day 07 · 游戏也要管理状态:场景切换·资源加载·对象池实战
前端·javascript·游戏
liulian09164 小时前
【Flutter For OpenHarmony】Flutter 三方库 flutter_secure_storage 的鸿蒙化适配指南
flutter·华为·学习方法·harmonyos
liulian09165 小时前
【Flutter For OpenHarmony】Flutter 三方库 flutter_local_notifications 的鸿蒙化适配指南
flutter·华为·学习方法·harmonyos
游了个戏5 小时前
微信小游戏 vs QQ小游戏:表面是兄弟,骨子里是两套完全不同的玩法
人工智能·游戏·微信
IntMainJhy5 小时前
【Flutter 三方库 Provider 】flutter for open harmony的鸿蒙化适配与实战指南✨
flutter·华为·harmonyos
Swift社区5 小时前
鸿蒙游戏,会不会重演微信小游戏的爆发?
游戏·微信·harmonyos
前端不太难6 小时前
鸿蒙游戏的 CI/CD 方案
游戏·ci/cd·harmonyos