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));
}
}