(摘) Flame Flutter中的2D游戏引擎

声明:内容源自网络,版权归原作者所有。若有侵权请在网页聊天中联系我

flame Github Flame 是一个开源的基于 Flutter 的游戏引擎,Flame 引擎的目的是为使用 Flutter 开发的游戏会遇到的常见问题提供一套完整的解决方案

看示例最简单,当然得先引入框架flame

啥也没有的主框架

import 'package:flame/game.dart';

class CustomGame extends FlameGame{
  @override
  // 渲染
  void render(Canvas canvas){
    super.render(canvas);
  }
  
  @override
  // 更新,dt是时间间隔,单位是秒,即隔多久调用一次update和render。
  // 在60FPS下,dt就等于0.016
  void update(double dt) {
    super.update(dt);
  }

}

void main() {
  final game = CustomGame();
  runApp(GameWidget(game: game));
}

一个移动的小球

import 'package:flutter/material.dart';
import 'package:flame/game.dart';

void main() async {
  final game = CustomGame();
  runApp(GameWidget(game: game));
}

class CustomGame extends FlameGame {
  Offset circleCenter = const Offset(0, 0);
  final Paint paint = Paint()..color = Colors.yellow;

  @override
  // 渲染了一个球体
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawCircle(circleCenter, 20, paint);
  }

  @override
  // 刷新,更新了球的中心位置
  void update(double dt) {
    super.update(dt);
    circleCenter = circleCenter.translate(1, 1);
  }
}

只有一个背景

class StickGame extends FlameGame{
  final Paint paint = Paint()..color = const Color.fromARGB(255, 35, 36, 38);
  final Path canvasPath = Path();
  
  @override
  Future<void>? onLoad() async{
    canvasPath.addRect(Rect.fromLTWH(0, 0, canvasSize.x, canvasSize.y));  // 根据画布大小添加一个矩形
    return super.onLoad();
  }
  
  @override
  void render(Canvas canvas){
    super.render(canvas);
    canvas.drawPath(canvasPath, paint); // 使用paint颜色,渲染路径
  }
}

在背景上画个圆,另起一“类”

import 'dart:ui';
import 'package:flutter/material.dart';

class TargetComponent {
  final Vector2 position;
  final double radius;
  late Paint paint = Paint()..color = Colors.greenAccent;

  TargetComponent({required this.position, this.radius = 20});  // 定义了两个变量,位置(必须)和半径

  void render(Canvas canvas){  
     canvas.drawCircle(position.toOffset(), radius, paint);  // 画布画圆
  }
}

能够拖动的圆


import 'package:flame/input.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() async {
  final game = StickGame();
  runApp(GameWidget(game: game));
}

// 主类 -------------------------------------------------
class StickGame extends FlameGame with HasDraggables {
  late TargetComponent target; // 引用目标组件,就是那个球
  final Paint paint = Paint()..color = const Color.fromARGB(255, 35, 36, 38); // 绘画的颜色
  final Path canvasPath = Path(); // 全局路径
  bool isDrag = false; // 拖动中

  @override
  // 载入事件
  Future<void>? onLoad() async {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // 全屏
    canvasPath.addRect(Rect.fromLTWH(0, 0, canvasSize.x, canvasSize.y)); // 一个矩形,渲染用于背景
    target = TargetComponent(position: Vector2(canvasSize.x / 2, canvasSize.y / 2), radius: 30); // 一个圆
    return super.onLoad();
  }

  @override
  // 渲染事件
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawPath(canvasPath, paint); // 渲染路径(onLoad中的矩形区域)
    target.render(canvas); // 调用TargetComponent类的渲染
  }

  // 以下关于拖动----
  @override
  // 拖动开始
  void onDragStart(int pointerId, DragStartInfo info) {
    super.onDragStart(pointerId, info);
    // 判断拖动的点是否在画布范围内
    if (target.path.contains(info.eventPosition.game.toOffset())) {
      isDrag = true;
    }
  }

  @override
  // 拖动过程中
  void onDragUpdate(int pointerId, DragUpdateInfo info) {
    super.onDragUpdate(pointerId, info);
    var eventPosition = info.eventPosition.game; // 事件的位置
    // 判断是不是在小球位置范围
    if (eventPosition.x < target.radius ||
        eventPosition.x > canvasSize.x - target.radius ||
        eventPosition.y < target.radius ||
        eventPosition.y > canvasSize.y - target.radius) {
      return;
    }
    // 在拖动过程中,调用目标(小球)的拖动更新事件
    if (isDrag) {
      target.onDragUpdate(pointerId, info);
    }
  }

  @override
  // 取消拖动
  void onDragCancel(int pointerId) {
    super.onDragCancel(pointerId);
    isDrag = false;
  }

  @override
  // 拖动结束
  void onDragEnd(int pointerId, DragEndInfo info) {
    super.onDragEnd(pointerId, info);
    isDrag = false;
  }
}

// 画圆 -----------------------------------------------------------
class TargetComponent {
  final Vector2 position; // 位置
  final double radius; // 半径
  late Paint paint = Paint()..color = Colors.greenAccent; // 颜色

  TargetComponent({required this.position, this.radius = 20});

  late Path path = Path()..addOval(Rect.fromLTWH(position.x - radius, position.y - radius, radius * 2, radius * 2)); // 小球范围, 用于确定小球位置

  void render(Canvas canvas) {
    canvas.drawCircle(position.toOffset(), radius, paint); // 画个球
  }

  // 拖动的时候刷新
  void onDragUpdate(int pointerId, DragUpdateInfo info) {
    var eventPosition = info.eventPosition.game; // 事件位置
    position.setValues(eventPosition.x, eventPosition.y); // 将事件位置设为小球位置
    _updatePath(); // 更新小球位置数据
  }

  // 更新小球位置(数据)
  void _updatePath() {
    path.reset();
    path.addOval(Rect.fromLTWH(position.x - radius, position.y - radius, radius * 2, radius * 2));
  }
}

完整的游戏,尽可能的作了些注释。源码在这里

//
//
//

import 'dart:math';

import 'package:flame/input.dart';
import 'package:flame/game.dart';
import 'package:flame/timer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() async {
  final game = StickGame();
  runApp(GameWidget(game: game));
}

// 主类 -------------------------------------------------
class StickGame extends FlameGame with HasDraggables, HasTappables {
  late TargetComponent target; // 引用目标组件,就是那个球
  final Paint paint = Paint()..color = const Color.fromARGB(255, 35, 36, 38); // 绘画的颜色
  final Path canvasPath = Path(); // 全局路径
  bool isDrag = false; // 拖动中
  late Timer timer;
  List<BulletComponent> bullets = [];
  double seconds = 0; // 计时
  bool isRunning = true; // 运行中
  late TextComponent score;
  late TextComponent restartText;

  // 重新开始
  void restart() {
    isRunning = true;
    bullets.clear();
    target.resetPosition();
    score.position.setValues(40, 40);
    score.textSize = 30;
    seconds = 0;
  }

  // 停止
  void stop() {
    isRunning = false;
    restartText.text = "RESTART";
    score.position.setValues(restartText.position.x, restartText.position.y - 80);
    score.text = "${seconds.toInt()}s";
    score.textSize = 40;
  }

  // 碰撞检查
  bool collisionCheck(BulletComponent bullet) {
    var tempPath = Path.combine(PathOperation.intersect, target.path, bullet.path);
    return tempPath.getBounds().width > 0;
  }

  // 回收(超出屏幕)
  void checkBullets() {
    var removeBullets = <BulletComponent>[];
    for (var bullet in bullets) {
      if (!canvasPath.contains(bullet.position.toOffset())) {
        removeBullets.add(bullet);
      }
    }
    bullets.removeWhere((element) => removeBullets.contains(element));
  }

  @override
  // 点击重新开始
  void onTapUp(int pointerId, TapUpInfo info) {
    super.onTapUp(pointerId, info);
    if (!isRunning && restartText.path.contains(info.eventPosition.game.toOffset())) {
      restart();
    }
  }

  @override
  // 载入事件
  Future<void>? onLoad() async {
    timer = Timer(0.1, onTick: () {
      checkBullets(); // 原本应该在渲染处检测回收,放这里应该更节能
      // 控制小球总数量
      if (bullets.length < 60) {
        createBullet();
      }
    }, repeat: true);

    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // 全屏
    canvasPath.addRect(Rect.fromLTWH(0, 0, canvasSize.x, canvasSize.y)); // 一个矩形,渲染用于背景
    target = TargetComponent(position: Vector2(canvasSize.x / 2, canvasSize.y / 2)); // 一个圆
    score = TextComponent(position: Vector2(40, 40), text: "0", textSize: 30); // 成绩
    restartText = TextComponent(position: Vector2(canvasSize.x / 2, canvasSize.y / 2), text: "START", textSize: 50); // 开始字样

    return super.onLoad();
  }

  @override
  // 渲染事件
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawPath(canvasPath, paint); // 渲染路径(onLoad中的矩形区域)
    target.render(canvas); // 调用TargetComponent类的渲染
    for (var bullet in bullets) {
      bullet.render(canvas);
    }
    score.render(canvas);
    // 如果没有运行,渲染重新开始字样
    if (!isRunning) {
      restartText.render(canvas);
    }
  }

  @override
  void update(double dt) {
    super.update(dt);
    for (var bullet in bullets) {
      bullet.update(dt);
    }
    if (isRunning) {
      timer.update(dt);
      seconds += dt; // 计时
      // 碰撞或更新
      for (var bullet in bullets) {
        if (collisionCheck(bullet)) {
          stop();
          return;
        } else {
          bullet.update(dt);
        }
      }
    }
  }

  void createBullet() {
    var random = Random(); //随机数生成类
    var radius = random.nextInt(8) + 2; // 随机半径

    // 计算位置
    // 是否在水平方向上,即画布的顶部和底部
    bool isHorizontal = random.nextBool();
    int x = isHorizontal
        ? random.nextInt(canvasSize.x.toInt())
        : random.nextBool()
            ? radius
            : canvasSize.x.toInt() - radius;
    int y = isHorizontal
        ? random.nextBool()
            ? radius
            : canvasSize.y.toInt() - radius
        : random.nextInt(canvasSize.y.toInt());
    var position = Vector2(x.toDouble(), y.toDouble());

    // 计算角度
    var angle = atan2(y - target.position.y, x - target.position.x);

    // 计算速度
    var speed = seconds / 40 + 0.1;
    bullets.add(BulletComponent(position: position, angle: angle, radius: radius.toDouble(), speed: speed));
  }

  // 以下关于拖动----
  @override
  // 拖动开始
  void onDragStart(int pointerId, DragStartInfo info) {
    super.onDragStart(pointerId, info);
    // 判断拖动的点是否在画布范围内
    if (target.path.contains(info.eventPosition.game.toOffset())) {
      isDrag = true;
    }
  }

  @override
  // 拖动过程中
  void onDragUpdate(int pointerId, DragUpdateInfo info) {
    super.onDragUpdate(pointerId, info);
    var eventPosition = info.eventPosition.game; // 事件的位置
    // 判断是不是在小球位置范围
    if (eventPosition.x < target.radius ||
        eventPosition.x > canvasSize.x - target.radius ||
        eventPosition.y < target.radius ||
        eventPosition.y > canvasSize.y - target.radius) {
      return;
    }
    // 在拖动过程中,调用目标(小球)的拖动更新事件
    if (isDrag) {
      target.onDragUpdate(pointerId, info);
    }
  }

  @override
  // 取消拖动
  void onDragCancel(int pointerId) {
    super.onDragCancel(pointerId);
    isDrag = false;
  }

  @override
  // 拖动结束
  void onDragEnd(int pointerId, DragEndInfo info) {
    super.onDragEnd(pointerId, info);
    isDrag = false;
  }
}

// 画圆 -----------------------------------------------------------
class TargetComponent {
  final Vector2 position; // 位置
  final double radius; // 半径
  late Paint paint = Paint()..color = Colors.greenAccent; // 颜色
  final Vector2 originPosition;

  TargetComponent({required this.position, this.radius = 20}) : originPosition = Vector2(position.x, position.y);

  late Path path = Path()..addOval(Rect.fromLTWH(position.x - radius, position.y - radius, radius * 2, radius * 2)); // 小球范围, 用于确定小球位置

  void render(Canvas canvas) {
    canvas.drawCircle(position.toOffset(), radius, paint); // 画个球
  }

  // 拖动的时候刷新
  void onDragUpdate(int pointerId, DragUpdateInfo info) {
    var eventPosition = info.eventPosition.game; // 事件位置
    position.setValues(eventPosition.x, eventPosition.y); // 将事件位置设为小球位置
    _updatePath(); // 更新小球位置数据
  }

  // 更新小球位置(数据)
  void _updatePath() {
    path.reset();
    path.addOval(Rect.fromLTWH(position.x - radius, position.y - radius, radius * 2, radius * 2));
  }

  void resetPosition() {
    position.setValues(originPosition.x, originPosition.y);
    _updatePath();
  }
}

// 子弹 ------------------------------------------------------
class BulletComponent {
  final Vector2 position; // 位置
  final double speed; // 速度
  final double angle; // 角度
  final double radius; // 半径
  late Paint paint = Paint()..color = Colors.orangeAccent; // 颜色
  late Path path = Path()..addOval(Rect.fromLTWH(position.x - radius, position.y - radius, radius * 2, radius * 2));

  BulletComponent({required this.position, this.speed = 5, this.angle = 0, this.radius = 10});

  void render(Canvas canvas) {
    canvas.drawCircle(position.toOffset(), radius, paint);
  }

  void update(double dt) {
    position.setValues(position.x - cos(angle) * speed, position.y - sin(angle) * speed);
    path.reset();
    path.addOval(Rect.fromLTWH(position.x - radius, position.y - radius, radius * 2, radius * 2));
  }
}

// 文字 ------------------------------------------------------------
class TextComponent {
  final Vector2 position;
  String text;
  final Color textColor;
  double textSize;

  final Path path = Path();

  TextComponent({required this.position, required this.text, this.textColor = Colors.white, this.textSize = 40});

  void render(Canvas canvas) {
    var textPainter = TextPainter(
        text: TextSpan(text: text, style: TextStyle(fontSize: textSize, color: textColor)),
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr);
    textPainter.layout(); // 进行布局
    textPainter.paint(canvas, Offset(position.x - textPainter.width / 2, position.y - textPainter.height / 2)); // 进行绘制
    path.reset();
    path.addRect(
        Rect.fromLTWH(position.x - textPainter.width / 2, position.y - textPainter.height / 2, textPainter.width, textPainter.height));
  }
}

相关文章