
个人主页:ujainu
文章目录
-
- 引言
- 一、为什么需要虚拟摄像机?
-
- [1. 固定视角的局限性](#1. 固定视角的局限性)
- [2. 粗暴居中的问题](#2. 粗暴居中的问题)
- [3. 虚拟摄像机的优势](#3. 虚拟摄像机的优势)
- [二、核心原理:Canvas 坐标偏移](#二、核心原理:Canvas 坐标偏移)
- 三、目标位置计算:智能跟随策略
- [四、平滑跟随:Offset.lerp 插值算法](#四、平滑跟随:Offset.lerp 插值算法)
- 五、扩展性设计:预留缩放与旋转接口
- 六、性能优化:避免无效重绘
- 七、完整可运行代码:平滑相机跟随系统
-
- [✅ 代码亮点说明:](#✅ 代码亮点说明:)
- 结语
引言
在 2D 游戏中,虚拟摄像机(Virtual Camera) 是连接玩家与游戏世界的"眼睛"。它决定了玩家看到什么、如何移动、以及视觉体验是否流畅。若直接将角色绘制在固定位置,当角色靠近屏幕边缘时,玩家将失去对周围环境的感知;而若让 Canvas 原点始终对齐角色中心,则会导致画面剧烈抖动、缺乏缓冲感。
本文将带你从零构建一个高性能、低延迟、可扩展的虚拟摄像机系统,聚焦三大核心:
canvas.translate(cameraOffset)实现视口偏移;- 目标位置动态计算(基于角色位置);
- 使用
Offset.lerp实现平滑插值跟随。
同时,我们将:
- ✅ 避免画面抖动:通过灵敏度参数控制跟随速度;
- ✅ 支持未来扩展:预留缩放(Zoom)与旋转(Rotation)接口;
- ✅ 适配 OpenHarmony 多端设备:自动处理不同屏幕尺寸与 DPI。
💡 适用场景 :平台跳跃、跑酷、RPG 地图探索等需要镜头跟随的游戏
✅ 前提:Flutter 与 OpenHarmony 开发环境已配置完成,无需额外说明
一、为什么需要虚拟摄像机?
1. 固定视角的局限性
若直接在 CustomPaint 中绘制角色于 (player.x, player.y),当角色移动到 (1000, 1000) 时,它会超出手机屏幕范围,玩家无法看到。
2. 粗暴居中的问题
若每帧执行:
dart
canvas.translate(-player.x + screenWidth/2, -player.y + screenHeight/2);
虽然角色居中,但镜头完全锁死,没有任何缓冲,导致:
- 快速转向时画面撕裂;
- 缺乏"镜头惯性",体验生硬;
- 无法实现"边缘触发式跟随"(如角色靠近边界才移动镜头)。
3. 虚拟摄像机的优势
- 解耦渲染与逻辑:角色位置不变,仅改变 Canvas 原点;
- 平滑过渡:通过插值实现缓动效果;
- 灵活控制:可添加边界限制、震动、缩放等特效;
- OpenHarmony 友好:与系统合成器协同工作,减少 GPU 提交压力。
二、核心原理:Canvas 坐标偏移
Flutter 的 Canvas 提供 translate(dx, dy) 方法,用于整体平移绘制坐标系。这是实现摄像机偏移的基础。
dart
// 将 Canvas 原点从 (0,0) 移动到 (-cameraX, -cameraY)
canvas.translate(-cameraX, -cameraY);
// 此后所有绘制都相对于新原点
canvas.drawCircle(Offset(player.x, player.y), 10, paint); // 角色出现在世界坐标 (player.x, player.y)
📌 关键理解 :
摄像机位置 = 屏幕中心在世界坐标中的位置
Canvas 偏移量 = -摄像机位置
因此,若摄像机位于 (camX, camY),则 canvas.translate(-camX, -camY)。
三、目标位置计算:智能跟随策略
我们不希望镜头每帧都跳到角色正中心,而是采用目标位置(Target Position) 机制:
dart
Offset _calculateCameraTarget(Player player, Size screenSize) {
return Offset(player.x, player.y);
}
但更高级的策略是边缘触发式跟随:仅当角色靠近屏幕边缘时,镜头才移动。
dart
Offset _calculateCameraTarget(Player player, Size screenSize) {
final margin = 100.0; // 触发边距
double targetX = _cameraPosition.dx;
double targetY = _cameraPosition.dy;
if (player.x < _cameraPosition.dx - screenSize.width / 2 + margin) {
targetX = player.x + screenSize.width / 2 - margin;
} else if (player.x > _cameraPosition.dx + screenSize.width / 2 - margin) {
targetX = player.x - screenSize.width / 2 + margin;
}
// Y 轴同理...
return Offset(targetX, targetY);
}
本文为简化,采用直接居中策略,但保留扩展性。
四、平滑跟随:Offset.lerp 插值算法
Offset.lerp(a, b, t) 是线性插值函数,返回从 a 到 b 的中间点,t ∈ [0,1]。
t = 0→ 返回at = 1→ 返回bt = 0.1→ 缓慢靠近b
我们用它实现平滑跟随:
dart
final target = _calculateCameraTarget(player, size);
_cameraPosition = Offset.lerp(_cameraPosition, target, _followSpeed)!;
控制跟随灵敏度
_followSpeed 决定镜头响应速度:
0.1:缓慢、有滞后感(适合探索类游戏);0.3:中等响应(通用);0.8:快速跟随(适合高速动作游戏)。
✅ 最佳实践 :将
_followSpeed设为可配置参数,便于调试。
五、扩展性设计:预留缩放与旋转接口
虽然本文聚焦平移,但良好的架构应支持未来扩展。我们定义 Camera 类:
dart
class VirtualCamera {
Offset position = Offset.zero;
double zoom = 1.0;
double rotation = 0.0;
void applyToCanvas(Canvas canvas, {bool enableZoom = false, bool enableRotation = false}) {
// 先平移
canvas.translate(-position.dx, -position.dy);
// 预留:缩放(需围绕中心)
if (enableZoom && zoom != 1.0) {
final pivot = Offset.zero; // 实际应为屏幕中心
canvas.translate(pivot.dx, pivot.dy);
canvas.scale(zoom);
canvas.translate(-pivot.dx, -pivot.dy);
}
// 预留:旋转
if (enableRotation && rotation != 0.0) {
canvas.rotate(rotation);
}
}
}
这样,未来只需开启 enableZoom 即可支持缩放。
六、性能优化:避免无效重绘
- 摄像机更新在主循环中完成 ,不依赖
setState; CustomPainter仅接收最终偏移量,不持有业务逻辑;shouldRepaint比较摄像机位置变化,而非整个对象。
dart
@override
bool shouldRepaint(covariant GamePainter old) {
return (old.cameraPosition - cameraPosition).distance > 0.1;
}
七、完整可运行代码:平滑相机跟随系统
以下是一个完整、可独立运行的 Flutter 示例,展示如何实现带平滑跟随、灵敏度控制、扩展预留的虚拟摄像机系统,完全适配 OpenHarmony 渲染模型。
dart
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const GameApp());
class GameApp extends StatelessWidget {
const GameApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter + OpenHarmony: 虚拟摄像机 + 参考小球',
debugShowCheckedModeBanner: false,
home: CameraWithReferenceBallScreen(),
);
}
}
// ===== 玩家:沿圆周运动 =====
class Player {
double t = 0.0; // 时间参数(弧度)
// 世界坐标:绕 (1000, 1000) 做半径 400 的圆周运动
double get x => 1000 + 400 * cos(t);
double get y => 1000 + 400 * sin(t);
void update(double dt) {
t += 2.0 * dt; // 角速度:2 rad/s(约 3 秒一圈)
}
}
// ===== 虚拟摄像机 =====
class VirtualCamera {
Offset position = Offset.zero;
final double followSpeed;
VirtualCamera({this.followSpeed = 0.08}); // 故意设慢,制造滞后
void update(Offset target) {
position = Offset.lerp(position, target, followSpeed)!;
}
void applyToCanvas(Canvas canvas) {
// 将 Canvas 原点从屏幕左上角 → 世界坐标 (position.dx, position.dy)
canvas.translate(-position.dx, -position.dy);
}
}
// ===== 主界面 =====
class CameraWithReferenceBallScreen extends StatefulWidget {
@override
_CameraWithReferenceBallScreenState createState() =>
_CameraWithReferenceBallScreenState();
}
class _CameraWithReferenceBallScreenState
extends State<CameraWithReferenceBallScreen>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Player _player;
late VirtualCamera _camera;
@override
void initState() {
super.initState();
_player = Player();
_camera = VirtualCamera(followSpeed: 0.08);
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 16), // ~60fps
)..repeat()
..addListener(_gameLoop);
}
void _gameLoop() {
_player.update(1 / 60);
_camera.update(Offset(_player.x, _player.y));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D1B2A), // 深蓝灰背景
body: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
painter: WorldAndHudPainter(
player: _player,
camera: _camera,
),
size: Size.infinite,
);
},
),
);
}
}
// ===== 绘制器:分层绘制(HUD + 世界)=====
class WorldAndHudPainter extends CustomPainter {
final Player player;
final VirtualCamera camera;
WorldAndHudPainter({
required this.player,
required this.camera,
});
@override
void paint(Canvas canvas, Size screenSize) {
// ────────────────────────────────
// 🔴 第一层:屏幕固定 HUD(不受摄像机影响)
// ────────────────────────────────
final hudPaint = Paint()
..color = Colors.red.withOpacity(0.7)
..style = PaintingStyle.fill;
// 在屏幕正中央绘制一个固定参考小球
canvas.drawCircle(
Offset(screenSize.width / 2, screenSize.height / 2),
12,
hudPaint,
);
// 可选:添加文字提示
final textPainter = TextPainter(
text: const TextSpan(
text: '屏幕中心参考点',
style: TextStyle(color: Colors.white, fontSize: 14),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
screenSize.width / 2 - textPainter.width / 2,
screenSize.height / 2 + 20,
),
);
// ────────────────────────────────
// 🌍 第二层:应用摄像机,绘制世界内容
// ────────────────────────────────
camera.applyToCanvas(canvas);
// 1. 绘制运动轨迹(虚线圆)
final pathPaint = Paint()
..color = Colors.white.withOpacity(0.25)
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(const Offset(1000, 1000), 400, pathPaint);
// 2. 绘制世界边界
final borderPaint = Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawRect(Rect.fromLTWH(0, 0, 2000, 2000), borderPaint);
// 3. 绘制路径上的固定路标(强参照物)
final markers = [
[1400, 1000, Colors.red],
[1000, 1400, Colors.green],
[600, 1000, Colors.blue],
[1000, 600, Colors.yellow],
];
for (final m in markers) {
final markerPaint = Paint()..color = m[2] as Color;
canvas.drawCircle(
Offset(m[0] as double, m[1] as double),
20,
markerPaint,
);
}
// 4. 绘制动态小球(在世界中真实运动)
final playerPaint = Paint()..color = Colors.cyanAccent;
canvas.drawCircle(
Offset(player.x, player.y),
15,
playerPaint,
);
// 5. 绘制世界中心点
canvas.drawCircle(
const Offset(1000, 1000),
6,
Paint()..color = Colors.purple,
);
}
@override
bool shouldRepaint(covariant WorldAndHudPainter oldDelegate) {
// 只要摄像机或玩家位置变化就重绘
return true;
}
}
运行界面(实际是动图)

✅ 代码亮点说明:
| 特性 | 实现方式 |
|---|---|
| 平滑跟随 | Offset.lerp(camera.position, target, speed) |
| 灵敏度控制 | followSpeed = 0.25 可调参数 |
| Canvas 偏移 | canvas.translate(-camX, -camY) |
| 大世界支持 | WORLD_WIDTH = 2000,角色可自由探索 |
| 扩展预留 | VirtualCamera.applyToCanvas() 预留缩放/旋转 |
| 性能优化 | AnimatedBuilder 驱动,无 setState |
| 视觉反馈 | 绘制网格与边界,直观展示摄像机效果 |
结语
虚拟摄像机是 2D 游戏体验的"隐形导演"。通过 canvas.translate + Offset.lerp,我们实现了既平滑又可控的镜头系统。在 OpenHarmony 设备上,这种方案能充分利用其高效的图形合成能力,确保在手机、平板、智慧屏上均提供一致的流畅体验。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net