用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)

使用Flutter开发桌上弹球游戏可以使用Flutter自带的绘图(CanvasCustomPaint)API,以下是实现过程的完整攻略。

使用Flutter开发桌上弹球游戏可以使用Flutter自带的绘图(Canvas&CustomPaint)API,以下是实现过程的完整攻略。

步骤1:创建Flutter项目

首先,在电脑上安装Flutter开发环境,并通过Flutter命令行工具创建新项目。

flutter create tabletop_pinball_game

在创建完毕后,进入项目目录。

cd tabletop_pinball_game

步骤2:添加依赖

在pubspec.yaml文件中添加“flame”依赖,用于简化游戏开发过程。

dependencies:
  flutter:
    sdk: flutter
  flame: ^0.27.1

在完成添加后运行以下命令:

flutter pub get

步骤3:创建游戏主界面

在lib目录下创建新的文件“game.dart”,在其中添加以下内容:

import 'dart:ui';
import 'package:flame/game.dart';

class PinballGame extends Game {
  @override
  void render(Canvas canvas) {
    // 画背景
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = Color(0xFFB3E5FC));
  }

  @override
  void update(double dt) {
    // 更新游戏状态
  }

  @override
  void resize(Size size) {
    // 游戏界面大小发生改变时调用
    super.resize(size);
  }
}

在这里,我们使用了CustomPaint中的Canvas进行了简单的绘图,通过渲染矩形实现了游戏的背景图。

步骤4:运行游戏

在使用VSCode等Flutter开发工具打开项目,选择游戏模拟器或连接真实手机进行测试即可。此时,游戏会显示一个蓝色的背景画面。

步骤5:添加球拍与弹球

在game.dart文件中,定义弹球及球拍:

import 'dart:ui';
import 'package:flame/components/component.dart';
import 'package:flame/game.dart';

class PinballGame extends Game {
  Paddle paddle; // 球拍
  Ball ball; // 弹球
  @override
  void render(Canvas canvas) {
    // 绘制球拍和弹球
    paddle.render(canvas);
    ball.render(canvas);
    // 画背景
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = Color(0xFFB3E5FC));
  }

  @override
  void update(double dt) {
    // 更新游戏状态
    paddle.update(dt);
    ball.update(dt);
  }

  @override
  void resize(Size size) {
    // 游戏界面大小发生改变时调用
    super.resize(size);
    paddle = Paddle(size);
    ball = Ball();
  }
}


class Paddle extends PositionComponent {
  Paddle(this.gameSize);
  Size gameSize;
  static const double paddleWidth = 100;
  static const double paddleHeight = 20;

  @override
  void render(Canvas c) {
    // 绘制球拍
    c.drawRect(Rect.fromLTWH(x, y, paddleWidth, paddleHeight), Paint()..color = Color(0xFFFFA726));
  }

  @override
  void update(double t) {
    x += dx * t;
    if (x < 0) {
      x = 0;
    }
    if (x > gameSize.width - paddleWidth) {
      x = gameSize.width - paddleWidth;
    }
  }

  void move(double displacement) {
    // 移动球拍
    dx = displacement;
  }
}


class Ball extends PositionComponent {
  static const double ballSize = 20;
  static const double ballSpeed = 500;
  Ball() {
    x = 0;
    y = 0;
    width = ballSize;
    height = ballSize;
  }
  double speedX = ballSpeed;
  double speedY = ballSpeed;

  @override
  void render(Canvas c) {
    // 绘制弹球
    c.drawCircle(Offset(x + ballSize / 2, y + ballSize / 2), ballSize / 2, Paint()..color = Color(0xFFFF7043));
  }

  @override
  void update(double t) {
    x += speedX * t;
    y += speedY * t;
    // 碰到左右墙壁
    if (x < 0 || x > (gameSize.width - ballSize)) {
      speedX = -speedX;
    }
    // 碰到顶部
    if (y < 0) {
      speedY = -speedY;
    }
  }
}

在这里,我们通过定义Ball与Paddle两个类,实现了游戏中的弹球与球拍。

步骤6:移动球拍

在game.dart文件中,重载了onPanUpdate方法,实现了球拍通过手指拖动进行位置的移动。

import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flame/components/component.dart';
import 'package:flame/game.dart';

class PinballGame extends Game {
  Paddle paddle; // 球拍
  Ball ball; // 弹球

  @override
  void render(Canvas canvas) {
    // 绘制球拍和弹球
    paddle.render(canvas);
    ball.render(canvas);
    // 画背景
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = Color(0xFFB3E5FC));
  }

  @override
  void update(double dt) {
    // 更新游戏状态
    paddle.update(dt);
    ball.update(dt);
  }

  @override
  void resize(Size size) {
    // 游戏界面大小发生改变时调用
    super.resize(size);
    paddle = Paddle(size);
    ball = Ball();
  }

  void movePaddle(DragUpdateDetails details) {
    final touchPosition = details.globalPosition;
    if (touchPosition != null) {
      paddle.move(touchPosition.dx - (paddleWidth / 2));
    }
  }

  void stopPaddle(DragEndDetails details) {
    paddle.move(0);
  }
}


class Paddle extends PositionComponent {
  Paddle(this.gameSize);
  Size gameSize;
  static const double paddleWidth = 100;
  static const double paddleHeight = 20;
  double dx = 0;

  @override
  void render(Canvas c) {
    // 绘制球拍
    c.drawRect(Rect.fromLTWH(x, y, paddleWidth, paddleHeight), Paint()..color = Color(0xFFFFA726));
  }

  @override
  void update(double t) {
    x += dx * t;
    if (x < 0) {
      x = 0;
    }
    if (x > gameSize.width - paddleWidth) {
      x = gameSize.width - paddleWidth;
    }
  }

  void move(double displacement) {
    // 移动球拍
    dx = displacement;
  }
}


class Ball extends PositionComponent {
  static const double ballSize = 20;
  static const double ballSpeed = 500;
  Ball() {
    x = 0;
    y = 0;
    width = ballSize;
    height = ballSize;
  }
  double speedX = ballSpeed;
  double speedY = ballSpeed;

  @override
  void render(Canvas c) {
    // 绘制弹球
    c.drawCircle(Offset(x + ballSize / 2, y + ballSize / 2), ballSize / 2, Paint()..color = Color(0xFFFF7043));
  }

  @override
  void update(double t) {
    x += speedX * t;
    y += speedY * t;
    // 碰到左右墙壁
    if (x < 0 || x > (gameSize.width - ballSize)) {
      speedX = -speedX;
    }
    // 碰到顶部
    if (y < 0) {
      speedY = -speedY;
    }
  }
}

在这里,我们使用了手势Recognizer,通过处理拖动手势的事件,在TouchPosition不为空时,实现了球拍的移动。

示例1:添加足球

在game.dart文件中,定义新的Soccer类,为游戏增加足球元素。

import 'dart:ui';
import 'package:flame/components/component.dart';
import 'package:flame/game.dart';

class PinballGame extends Game {
  Paddle paddle; // 球拍
  Ball ball; // 弹球
  Soccer soccer; // 足球
  @override
    void render(Canvas canvas) {
    // 绘制球拍、弹球及足球
    paddle.render(canvas);
    ball.render(canvas);
    soccer.render(canvas);
    // 画背景
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = Color(0xFFB3E5FC));
  }
  @override
  void update(double dt) {
    // 更新游戏状态
    paddle.update(dt);
    ball.update(dt);
    soccer.update(dt);
  }
  @override
  void resize(Size size) {
    // 游戏界面大小发生改变时调用
    super.resize(size);
    paddle = Paddle(size);
    ball = Ball();
    soccer = Soccer();
  }
}
class Soccer extends PositionComponent {
  static const double soccerSize = 30;
  static const double soccerSpeed = 100;
  Soccer() {
    x = gameSize.width / 2 - soccerSize / 2;
    y = 0;
    width = soccerSize;
    height = soccerSize;
  }
  double speedY = soccerSpeed;
  @override
  void render(Canvas c) {
    // 绘制足球
    final ballImg = Image.asset("assets/ball.png");
    c.drawImage(ballImg, Rect.fromLTWH(x, y, soccerSize, soccerSize), Paint());
  }
  @override
  void update(double t) {
    y += speedY * t;
  }
}

在这里,我们新定义了一个Soccer类,继承了PositionComponent。在update方法中,让足球以不断加速的速度向下移动。并在render方法中,通过Image.asset()方法取出指定路径的足球图像,将其渲染在画布上。

示例2:添加背景音乐

在pubspec.yaml中添加assets:资源文件夹,将音乐文件复制到assets目录下。完成后,在game.dart中添加以下代码:

import 'dart:ui';
import 'package:flame/components/component.dart';
import 'package:flame/game.dart';
import 'package:flame/flame.dart';
import 'package:flame_audio/flame_audio.dart';

class PinballGame extends Game {
  Paddle paddle; // 球拍
  Ball ball; // 弹球
  Soccer soccer; // 足球
  bool isBGMReady = false; // 背景音乐是否准备就绪
  @override
  void render(Canvas canvas) {
    // 绘制球拍、弹球及足球
    paddle.render(canvas);
    ball.render(canvas);
    soccer.render(canvas);
    // 画背景
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), Paint()..color = Color(0xFFB3E5FC));
  }
  @override
  void update(double dt) {
    // 更新游戏状态
    paddle.update(dt);
    ball.update(dt);
    soccer.update(dt);
  }
  @override
  void resize(Size size) {
    // 游戏界面大小发生改变时调用
    super.resize(size);
    paddle = Paddle(size);
    ball = Ball();
    soccer = Soccer();
    // 加载背景音乐
    Flame.audio.load('bgm.mp3').then((_) {
      isBGMReady = true;
    });
  }
  @override
  void onAttach() {
    // 游戏启动时播放背景音乐
    if (isBGMReady) {
      FlameAudio.play('bgm.mp3', volume: 0.25, loop: true);
    }
  }
}

在这里,我们引入flame_audio和flame包中的Flame。在resize时,调用Flame.audio.load(filePath)加载背景音乐文件,通过onAttach中的播放代码来播放背景音乐。在FlameAudio.play()中,我们通过设置volume使音乐声音降低,loop进行重复播放。

在以上两个示例中,我们对游戏进行了足球元素的添加,以及播放了背景音乐。

本文标题为:用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)

基础教程推荐