مقدمه ای بر Flame with Flutter

۱. مقدمه

فلیم (Flame) یک موتور بازی دوبعدی مبتنی بر فلاتر (Flutter) است. در این آزمایشگاه کد، شما یک بازی با الهام از یکی از بازی‌های ویدیویی کلاسیک دهه 70، یعنی بازی Breakout ساخته استیو وزنیاک، خواهید ساخت. شما از کامپوننت‌های فلیم برای ترسیم چوب بیسبال، توپ و آجرها استفاده خواهید کرد. شما از افکت‌های فلیم برای متحرک‌سازی حرکت چوب بیسبال استفاده خواهید کرد و خواهید دید که چگونه می‌توان فلیم را با سیستم مدیریت حالت فلاتر ادغام کرد.

وقتی کامل شد، بازی شما باید شبیه این گیف متحرک باشد، البته کمی کندتر.

ضبط صفحه نمایش از یک بازی در حال انجام. سرعت بازی به طور قابل توجهی افزایش یافته است.

آنچه یاد خواهید گرفت

  • نحوه کار اصول اولیه Flame، با شروع از GameWidget .
  • نحوه استفاده از حلقه بازی.
  • نحوه کار Component Flame. آنها شبیه به Widget Flutter هستند.
  • نحوه برخورد با تصادم‌ها.
  • نحوه استفاده از Effect ها برای متحرک سازی Component ها.
  • نحوه‌ی قرار دادن Widget Flutter روی یک بازی Flame.
  • نحوه ادغام Flame با مدیریت حالت Flutter.

آنچه خواهید ساخت

در این آزمایشگاه کد، شما قرار است یک بازی دوبعدی با استفاده از Flutter و Flame بسازید. پس از اتمام، بازی شما باید شرایط زیر را داشته باشد:

  • قابلیت اجرا روی هر شش پلتفرمی که فلاتر پشتیبانی می‌کند: اندروید، iOS، لینوکس، macOS، ویندوز و وب
  • با استفاده از حلقه بازی Flame، حداقل ۶۰ فریم بر ثانیه را حفظ کنید.
  • از قابلیت‌های فلاتر مانند پکیج google_fonts و flutter_animate برای بازسازی حس بازی‌های آرکید دهه ۸۰ میلادی استفاده کنید.

۲. محیط فلاتر خود را تنظیم کنید

ویرایشگر

برای ساده‌سازی این آزمایشگاه کد، فرض بر این است که ویژوال استودیو کد (VS Code) محیط توسعه شماست. VS Code رایگان است و روی همه پلتفرم‌های اصلی کار می‌کند. ما از VS Code برای این آزمایشگاه کد استفاده می‌کنیم زیرا دستورالعمل‌ها به طور پیش‌فرض به میانبرهای مخصوص VS Code اشاره دارند. وظایف ساده‌تر می‌شوند: «روی این دکمه کلیک کنید» یا «این کلید را فشار دهید تا X انجام شود» به جای «برای انجام X، عمل مناسب را در ویرایشگر خود انجام دهید».

شما می‌توانید از هر ویرایشگری که دوست دارید استفاده کنید: اندروید استودیو، سایر IDEهای IntelliJ، Emacs، Vim یا Notepad++. همه آنها با Flutter کار می‌کنند.

کد VS به همراه مقداری کد Flutter

یک هدف توسعه‌ای انتخاب کنید

فلاتر برای پلتفرم‌های مختلف اپلیکیشن تولید می‌کند. اپلیکیشن شما می‌تواند روی هر یک از سیستم عامل‌های زیر اجرا شود:

  • آی‌او‌اس
  • اندروید
  • ویندوز
  • مک‌او‌اس
  • لینوکس
  • وب

انتخاب یک سیستم عامل به عنوان هدف توسعه، یک رویه رایج است. این سیستم عاملی است که برنامه شما در طول توسعه روی آن اجرا می‌شود.

طرحی که یک لپ‌تاپ و یک تلفن متصل به لپ‌تاپ را با کابل نشان می‌دهد. لپ‌تاپ به عنوان ... برچسب‌گذاری شده است.

برای مثال: فرض کنید از یک لپ‌تاپ ویندوزی برای توسعه اپلیکیشن Flutter خود استفاده می‌کنید. سپس اندروید را به عنوان هدف توسعه خود انتخاب می‌کنید. برای پیش‌نمایش اپلیکیشن خود، یک دستگاه اندروید را با کابل USB به لپ‌تاپ ویندوزی خود متصل می‌کنید و اپلیکیشن در حال توسعه شما روی آن دستگاه اندروید متصل یا در یک شبیه‌ساز اندروید اجرا می‌شود. می‌توانستید ویندوز را به عنوان هدف توسعه انتخاب کنید که اپلیکیشن در حال توسعه شما را به عنوان یک اپلیکیشن ویندوزی در کنار ویرایشگر شما اجرا می‌کند.

قبل از ادامه، انتخاب خود را انجام دهید. همیشه می‌توانید برنامه خود را بعداً روی سیستم‌عامل‌های دیگر اجرا کنید. انتخاب یک هدف توسعه، گام بعدی را هموارتر می‌کند.

نصب فلاتر

جدیدترین دستورالعمل‌های نصب SDK فلاتر را می‌توانید در docs.flutter.dev پیدا کنید.

دستورالعمل‌های موجود در وب‌سایت Flutter شامل نصب SDK و ابزارهای مرتبط با هدف توسعه و افزونه‌های ویرایشگر است. برای این codelab، نرم‌افزارهای زیر را نصب کنید:

  1. کیت توسعه نرم‌افزار فلاتر
  2. ویژوال استودیو کد با افزونه فلاتر
  3. نرم‌افزار کامپایلر برای هدف توسعه انتخابی شما. (برای هدف قرار دادن ویندوز به Visual Studio و برای هدف قرار دادن macOS یا iOS به Xcode نیاز دارید)

در بخش بعدی، اولین پروژه فلاتر خود را ایجاد خواهید کرد.

اگر نیاز به عیب‌یابی هرگونه مشکلی دارید، ممکن است برخی از این پرسش و پاسخ‌ها (از StackOverflow) برای عیب‌یابی مفید باشند.

سوالات متداول

۳. ایجاد یک پروژه

اولین پروژه فلاتر خود را ایجاد کنید

این شامل باز کردن VS Code و ایجاد الگوی برنامه Flutter در دایرکتوری مورد نظر شما می‌شود.

  1. ویژوال استودیو کد را اجرا کنید.
  2. پالت دستورات ( F1 یا Ctrl+Shift+P یا Shift+Cmd+P ) را باز کنید، سپس عبارت "flutter new" را تایپ کنید. وقتی ظاهر شد، دستور Flutter: New Project را انتخاب کنید.

کد VS با

  1. گزینه Empty Application را انتخاب کنید. یک دایرکتوری برای ایجاد پروژه خود انتخاب کنید. این دایرکتوری باید هر دایرکتوری باشد که نیازی به دسترسی‌های بالا نداشته باشد یا در مسیر آن فاصله وجود نداشته باشد. به عنوان مثال، دایرکتوری home یا C:\src\ از این دسته هستند.

کد VS با برنامه خالی که به عنوان بخشی از جریان برنامه جدید انتخاب شده نشان داده شده است

  1. نام پروژه خود را brick_breaker . در ادامه این آموزش فرض بر این است که شما نام برنامه خود را brick_breaker گذاشته‌اید.

کد VS با

فلاتر اکنون پوشه پروژه شما را ایجاد می‌کند و VS Code آن را باز می‌کند. اکنون محتویات دو فایل را با یک چارچوب اولیه از برنامه بازنویسی خواهید کرد.

برنامه اولیه را کپی و جایگذاری کنید

این کد نمونه ارائه شده در این codelab را به برنامه شما اضافه می‌کند.

  1. در پنل سمت چپ VS Code، روی Explorer کلیک کنید و فایل pubspec.yaml را باز کنید.

یک اسکرین‌شات ناقص از VS Code به همراه فلش‌هایی که محل فایل pubspec.yaml را مشخص می‌کنند

  1. محتویات این فایل را با موارد زیر جایگزین کنید:

pubspec.yaml

name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: "none"
version: 0.1.0

environment:
  sdk: ^3.8.0

dependencies:
  flutter:
    sdk: flutter
  flame: ^1.28.1
  flutter_animate: ^4.5.2
  google_fonts: ^6.2.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

فایل pubspec.yaml اطلاعات اولیه در مورد برنامه شما، مانند نسخه فعلی آن، وابستگی‌های آن و دارایی‌هایی که با آنها ارسال خواهد شد را مشخص می‌کند.

  1. فایل main.dart را در دایرکتوری lib/ باز کنید.

یک اسکرین‌شات ناقص از VS Code به همراه فلشی که محل فایل main.dart را نشان می‌دهد

  1. محتویات این فایل را با موارد زیر جایگزین کنید:

lib/main.dart

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

void main() {
  final game = FlameGame();
  runApp(GameWidget(game: game));
}
  1. این کد را اجرا کنید تا مطمئن شوید همه چیز درست کار می‌کند. باید یک پنجره جدید با پس‌زمینه سیاه خالی نمایش داده شود. بدترین بازی ویدیویی دنیا اکنون با سرعت ۶۰ فریم در ثانیه رندر می‌شود!

تصویری از پنجره‌ی کاملاً سیاهِ برنامه‌ی brick_breaker.

۴. بازی را بسازید

بازی را بزرگ کنید

یک بازی که در دو بعد (2D) انجام می‌شود، به یک منطقه بازی نیاز دارد. شما منطقه‌ای با ابعاد مشخص خواهید ساخت و سپس از این ابعاد برای اندازه‌گذاری سایر جنبه‌های بازی استفاده خواهید کرد.

روش‌های مختلفی برای چیدمان مختصات در فضای بازی وجود دارد. طبق یک قرارداد، می‌توانید جهت را از مرکز صفحه با مبدا (0,0) در مرکز صفحه اندازه‌گیری کنید، مقادیر مثبت، آیتم‌ها را در امتداد محور x به سمت راست و در امتداد محور y به سمت بالا حرکت می‌دهند. این استاندارد در مورد اکثر بازی‌های امروزی، به ویژه بازی‌هایی که شامل سه بعد هستند، صدق می‌کند.

رسم زمانی که بازی اصلی Breakout ساخته شد این بود که مبدا مختصات در گوشه بالا سمت چپ قرار گیرد. جهت مثبت x ثابت می‌ماند، هرچند y برعکس می‌شد. جهت مثبت x راست و y پایین بود. برای وفادار ماندن به آن دوران، این بازی مبدا مختصات را در گوشه بالا سمت چپ قرار می‌دهد.

فایلی به نام config.dart در دایرکتوری جدیدی به نام lib/src ایجاد کنید. این فایل در مراحل بعدی ثابت‌های بیشتری به دست خواهد آورد.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;

این بازی ۸۲۰ پیکسل عرض و ۱۶۰۰ پیکسل ارتفاع خواهد داشت. مقیاس ناحیه بازی متناسب با پنجره‌ای که در آن نمایش داده می‌شود، تغییر می‌کند، اما تمام اجزای اضافه شده به صفحه نمایش با این ارتفاع و عرض مطابقت دارند.

یک منطقه بازی ایجاد کنید

در بازی Breakout، توپ از دیوارهای منطقه بازی برمی‌گردد. برای ایجاد برخورد، ابتدا به یک مؤلفه PlayArea نیاز دارید.

  1. فایلی به نام play_area.dart در دایرکتوری جدیدی به نام lib/src/components ایجاد کنید.
  2. موارد زیر را به این فایل اضافه کنید.

lib/src/components/play_area.dart

import 'dart:async';

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

import '../brick_breaker.dart';

class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
  PlayArea() : super(paint: Paint()..color = const Color(0xfff2e8cf));

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}

در حالی که فلاتر Widget را دارد، فلیم Component را دارد. در حالی که برنامه‌های فلاتر شامل ایجاد درخت‌هایی از ویجت‌ها هستند، بازی‌های فلیم شامل نگهداری درخت‌هایی از کامپوننت‌ها هستند.

در اینجا یک تفاوت جالب بین Flutter و Flame وجود دارد. درخت ویجت Flutter یک توصیف زودگذر است که برای استفاده در به‌روزرسانی لایه RenderObject پایدار و تغییرپذیر ساخته شده است. اجزای Flame پایدار و تغییرپذیر هستند، با این انتظار که توسعه‌دهنده از این اجزا به عنوان بخشی از یک سیستم شبیه‌سازی استفاده کند.

اجزای Flame برای بیان مکانیک بازی بهینه شده‌اند. این آزمایشگاه کد با حلقه بازی که در مرحله بعدی نمایش داده می‌شود، شروع خواهد شد.

  1. برای کنترل بی‌نظمی، فایلی حاوی تمام کامپوننت‌های این پروژه اضافه کنید. یک فایل components.dart در lib/src/components ایجاد کنید و محتوای زیر را به آن اضافه کنید.

lib/src/components/components.dart

export 'play_area.dart';

دستور export نقش معکوس import را ایفا می‌کند. این دستور مشخص می‌کند که این فایل هنگام وارد شدن به فایل دیگر چه عملکردی را نشان می‌دهد. با اضافه شدن اجزای جدید در مراحل بعدی، ورودی‌های این فایل افزایش می‌یابد.

یک بازی شعله ایجاد کنید

برای محو کردن خطوط موج‌دار قرمز از مرحله قبل، یک زیرکلاس جدید برای FlameGame از کلاس Flame مشتق کنید.

  1. یک فایل با نام brick_breaker.dart در lib/src ایجاد کنید و کد زیر را به آن اضافه کنید.

lib/src/brick_breaker.dart

import 'dart:async';

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

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame {
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());
  }
}

این فایل، اقدامات بازی را هماهنگ می‌کند. در طول ساخت نمونه بازی، این کد، بازی را برای استفاده از رندر با وضوح ثابت پیکربندی می‌کند. اندازه بازی برای پر کردن صفحه‌ای که در آن قرار دارد تغییر می‌کند و در صورت نیاز، کادربندی حروف را اضافه می‌کند.

شما عرض و ارتفاع بازی را مشخص می‌کنید تا کامپوننت‌های فرزند، مانند PlayArea ، بتوانند خودشان را در اندازه مناسب تنظیم کنند.

در متد override شده‌ی onLoad ، کد شما دو عمل انجام می‌دهد.

  1. بالا سمت چپ را به عنوان نقطه مرجع برای منظره‌یاب تنظیم می‌کند. به طور پیش‌فرض، viewfinder از وسط ناحیه به عنوان نقطه مرجع برای (0,0) استفاده می‌کند.
  2. PlayArea به world اضافه می‌کند. جهان، دنیای بازی را نشان می‌دهد. تمام فرزندانش را از طریق تبدیل نمای CameraComponent نمایش می‌دهد.

بازی را روی صفحه نمایش بیاورید

برای مشاهده تمام تغییراتی که در این مرحله ایجاد کرده‌اید، فایل lib/main.dart خود را با تغییرات زیر به‌روزرسانی کنید.

lib/main.dart

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

import 'src/brick_breaker.dart';                                // Add this import

void main() {
  final game = BrickBreaker();                                  // Modify this line
  runApp(GameWidget(game: game));
}

بعد از انجام این تغییرات، بازی را مجدداً راه اندازی کنید. بازی باید شبیه شکل زیر باشد.

تصویری از پنجره برنامه brick_breaker که یک مستطیل به رنگ شنی در وسط آن قرار دارد.

در مرحله بعد، یک توپ به دنیا اضافه می‌کنید و آن را به حرکت درمی‌آورید!

۵. توپ را به نمایش بگذارید

کامپوننت توپ را ایجاد کنید

قرار دادن یک توپ متحرک روی صفحه نمایش شامل ایجاد یک جزء دیگر و اضافه کردن آن به دنیای بازی است.

  1. محتویات فایل lib/src/config.dart را به صورت زیر ویرایش کنید.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;                            // Add this constant

الگوی طراحی تعریف ثابت‌های نام‌گذاری‌شده به عنوان مقادیر مشتق‌شده، بارها در این آزمایشگاه کد بازگردانده خواهد شد. این به شما امکان می‌دهد gameWidth و gameHeight سطح بالا را تغییر دهید تا ببینید که ظاهر و حس بازی در نتیجه چگونه تغییر می‌کند.

  1. کامپوننت Ball را در فایلی به نام ball.dart در lib/src/components ایجاد کنید.

lib/src/components/ball.dart

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

class Ball extends CircleComponent {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
         radius: radius,
         anchor: Anchor.center,
         paint: Paint()
           ..color = const Color(0xff1e6091)
           ..style = PaintingStyle.fill,
       );

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }
}

پیش از این، شما PlayArea با استفاده از RectangleComponent تعریف کردید، بنابراین منطقی است که شکل‌های بیشتری وجود داشته باشند. CircleComponent ، مانند RectangleComponent ، از PositionedComponent مشتق شده است، بنابراین می‌توانید توپ را روی صفحه قرار دهید. مهمتر از آن، موقعیت آن را می‌توان به‌روزرسانی کرد.

این کامپوننت مفهوم velocity یا تغییر موقعیت در طول زمان را معرفی می‌کند. سرعت یک شیء Vector2 است زیرا سرعت هم سرعت و هم جهت را نشان می‌دهد . برای به‌روزرسانی موقعیت، متد update را که موتور بازی برای هر فریم فراخوانی می‌کند، لغو کنید. dt مدت زمان بین فریم قبلی و این فریم است. این به شما امکان می‌دهد تا با عواملی مانند نرخ فریم‌های مختلف (60 هرتز یا 120 هرتز) یا فریم‌های طولانی به دلیل محاسبات بیش از حد سازگار شوید.

به به‌روزرسانی position += velocity * dt توجه ویژه داشته باشید. اینگونه است که شما به‌روزرسانی یک شبیه‌سازی گسسته از حرکت را در طول زمان پیاده‌سازی می‌کنید.

  1. برای افزودن کامپوننت Ball به لیست کامپوننت‌ها، فایل lib/src/components/components.dart را به صورت زیر ویرایش کنید.

lib/src/components/components.dart

export 'ball.dart';                           // Add this export
export 'play_area.dart';

توپ را به جهان اضافه کنید

شما یک توپ دارید. آن را در دنیای بازی قرار دهید و طوری تنظیم کنید که در اطراف زمین بازی حرکت کند.

فایل lib/src/brick_breaker.dart را به صورت زیر ویرایش کنید.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;                                     // Add this import

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

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame {
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  final rand = math.Random();                                   // Add this variable
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(
      Ball(                                                     // Add from here...
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2(
          (rand.nextDouble() - 0.5) * width,
          height * 0.2,
        ).normalized()..scale(height / 4),
      ),
    );

    debugMode = true;                                           // To here.
  }
}

این تغییر، کامپوننت Ball را به world اضافه می‌کند. برای تنظیم position توپ در مرکز ناحیه نمایش، کد ابتدا اندازه بازی را نصف می‌کند، زیرا Vector2 دارای سربارگذاری عملگرها ( * و / ) برای مقیاس‌بندی Vector2 با یک مقدار اسکالر است.

تنظیم velocity توپ پیچیدگی بیشتری دارد. هدف این است که توپ را با سرعت معقول و در یک جهت تصادفی روی صفحه نمایش حرکت دهیم. فراخوانی متد normalized یک شیء Vector2 ایجاد می‌کند که در همان جهت Vector2 اصلی تنظیم شده است، اما به فاصله ۱ کاهش یافته است. این کار سرعت توپ را صرف نظر از جهتی که توپ می‌رود، ثابت نگه می‌دارد. سپس سرعت توپ به ۱/۴ ارتفاع بازی افزایش می‌یابد.

درست بدست آوردن این مقادیر مختلف مستلزم مقداری تکرار است که در صنعت بازی به عنوان تست بازی نیز شناخته می‌شود.

خط آخر، نمایشگر اشکال‌زدایی را روشن می‌کند که اطلاعات اضافی را برای کمک به اشکال‌زدایی به نمایشگر اضافه می‌کند.

وقتی بازی را اجرا کنید، باید چیزی شبیه به تصویر زیر باشد.

تصویری از پنجره برنامه brick_breaker با یک دایره آبی در بالای مستطیل شنی رنگ. دایره آبی با اعدادی که اندازه و مکان آن را روی صفحه نشان می‌دهند، حاشیه‌نویسی شده است.

هم کامپوننت PlayArea و هم کامپوننت Ball هر دو اطلاعات اشکال‌زدایی دارند، اما مات‌های پس‌زمینه، اعداد PlayArea را برش می‌دهند. دلیل اینکه همه چیز اطلاعات اشکال‌زدایی را نمایش می‌دهد این است که شما debugMode برای کل درخت کامپوننت فعال کرده‌اید. اگر این مفیدتر است، می‌توانید اشکال‌زدایی را فقط برای کامپوننت‌های انتخاب شده فعال کنید.

اگر بازی خود را چند بار از نو شروع کنید، ممکن است متوجه شوید که توپ آنطور که انتظار می‌رود با دیوارها برخورد نمی‌کند. برای رسیدن به این هدف، باید تشخیص برخورد را اضافه کنید، که در مرحله بعدی انجام خواهید داد.

۶. بالا و پایین پریدن

اضافه شدن قابلیت تشخیص برخورد

تشخیص برخورد، رفتاری را اضافه می‌کند که در آن بازی شما تشخیص می‌دهد چه زمانی دو شیء با یکدیگر برخورد می‌کنند.

برای افزودن تشخیص برخورد به بازی، مخلوط HasCollisionDetection را همانطور که در کد زیر نشان داده شده است به بازی BrickBreaker اضافه کنید.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

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

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(
      Ball(
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2(
          (rand.nextDouble() - 0.5) * width,
          height * 0.2,
        ).normalized()..scale(height / 4),
      ),
    );

    debugMode = true;
  }
}

این تابع، هیت‌باکس‌های کامپوننت‌ها را ردیابی می‌کند و در هر تیک بازی، فراخوانی‌های برخورد (collision callbacks) را فعال می‌کند.

برای شروع پر کردن هیت‌باکس‌های بازی، کامپوننت PlayArea را مطابق شکل زیر تغییر دهید:

lib/src/components/play_area.dart

import 'dart:async';

import 'package:flame/collisions.dart';                         // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';

class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
  PlayArea()
    : super(
        paint: Paint()..color = const Color(0xfff2e8cf),
        children: [RectangleHitbox()],                          // Add this parameter
      );

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}

اضافه کردن یک کامپوننت RectangleHitbox به عنوان فرزند RectangleComponent ، یک جعبه برخورد (hit box) برای تشخیص برخورد می‌سازد که با اندازه کامپوننت والد مطابقت دارد. یک سازنده کارخانه‌ای برای RectangleHitbox به نام relative وجود دارد که برای مواقعی که می‌خواهید یک جعبه برخورد (hitbox) کوچک‌تر یا بزرگ‌تر از کامپوننت والد باشد، استفاده می‌شود.

توپ را بپران.

تا اینجا، اضافه کردن تشخیص برخورد هیچ تفاوتی در گیم‌پلی ایجاد نکرده است. وقتی کامپوننت Ball را تغییر دهید، گیم‌پلی تغییر می‌کند. این رفتار توپ است که باید هنگام برخورد با PlayArea تغییر کند.

کامپوننت Ball را به صورت زیر تغییر دهید.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';                         // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';                                 // And this import
import 'play_area.dart';                                        // And this one too

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {   // Add these mixins
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
         radius: radius,
         anchor: Anchor.center,
         paint: Paint()
           ..color = const Color(0xff1e6091)
           ..style = PaintingStyle.fill,
         children: [CircleHitbox()],                            // Add this parameter
       );

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override                                                     // Add from here...
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        removeFromParent();
      }
    } else {
      debugPrint('collision with $other');
    }
  }                                                             // To here.
}

این مثال با اضافه شدن فراخوانی onCollisionStart تغییر عمده‌ای ایجاد می‌کند. سیستم تشخیص برخورد که در مثال قبلی به BrickBreaker اضافه شده بود، این فراخوانی را فراخوانی می‌کند.

ابتدا، کد بررسی می‌کند که آیا Ball با PlayArea برخورد کرده است یا خیر. این مورد فعلاً اضافی به نظر می‌رسد، زیرا هیچ مؤلفه دیگری در دنیای بازی وجود ندارد. این موضوع در مرحله بعدی، زمانی که یک خفاش به دنیای بازی اضافه می‌کنید، تغییر خواهد کرد. سپس، یک شرط else نیز اضافه می‌کند تا در صورت برخورد توپ با چیزهایی غیر از خفاش، آن را مدیریت کند. اگر مایل باشید، این یک یادآوری ملایم برای پیاده‌سازی منطق باقی‌مانده است.

وقتی توپ به دیوار پایینی برخورد می‌کند، در حالی که هنوز کاملاً در دید است، از سطح بازی ناپدید می‌شود. شما این اثر باستانی را در مرحله بعدی، با استفاده از قدرت جلوه‌های شعله، مدیریت خواهید کرد.

حالا که توپ به دیوارهای بازی برخورد می‌کند، مطمئناً مفید خواهد بود که به بازیکن یک چوب بیسبال بدهید تا با آن به توپ ضربه بزند...

۷. ضربه چوب بیسبال را به سمت توپ بگیرید

خفاش را ایجاد کنید

برای اضافه کردن چوب بیسبال برای نگه داشتن توپ در جریان بازی،

  1. تعدادی ثابت را به صورت زیر در فایل lib/src/config.dart وارد کنید.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;                               // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;                               // To here.

ثابت‌های batHeight و batWidth نیازی به توضیح ندارند. از سوی دیگر، ثابت batStep نیاز به توضیح بیشتری دارد. برای تعامل با توپ در این بازی، بازیکن می‌تواند چوب بیسبال را با ماوس یا انگشت، بسته به پلتفرم، بکشد یا از صفحه کلید استفاده کند. ثابت batStep میزان گام‌های چوب بیسبال را برای هر بار فشردن کلید جهت چپ یا راست تنظیم می‌کند.

  1. کلاس کامپوننت Bat را به صورت زیر تعریف کنید.

lib/src/components/bat.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';

class Bat extends PositionComponent
    with DragCallbacks, HasGameReference<BrickBreaker> {
  Bat({
    required this.cornerRadius,
    required super.position,
    required super.size,
  }) : super(anchor: Anchor.center, children: [RectangleHitbox()]);

  final Radius cornerRadius;

  final _paint = Paint()
    ..color = const Color(0xff1e6091)
    ..style = PaintingStyle.fill;

  @override
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawRRect(
      RRect.fromRectAndRadius(Offset.zero & size.toSize(), cornerRadius),
      _paint,
    );
  }

  @override
  void onDragUpdate(DragUpdateEvent event) {
    super.onDragUpdate(event);
    position.x = (position.x + event.localDelta.x).clamp(0, game.width);
  }

  void moveBy(double dx) {
    add(
      MoveToEffect(
        Vector2((position.x + dx).clamp(0, game.width), position.y),
        EffectController(duration: 0.1),
      ),
    );
  }
}

این کامپوننت چند قابلیت جدید را معرفی می‌کند.

اول اینکه، کامپوننت Bat یک PositionComponent است، نه RectangleComponent و نه CircleComponent . این یعنی این کد باید Bat را روی صفحه رندر کند. برای انجام این کار، تابع فراخوانی render را لغو می‌کند.

با نگاهی دقیق به فراخوانی canvas.drawRRect (رسم مستطیل گرد) ممکن است از خود بپرسید: «مستطیل کجاست؟» Offset.zero & size.toSize() از یک operator & overload روی کلاس dart:ui Offset که Rect ها را ایجاد می‌کند، استفاده می‌کنند. این خلاصه‌نویسی ممکن است در ابتدا شما را گیج کند، اما آن را مکرراً در کد سطح پایین‌تر Flutter و Flame خواهید دید.

دوم، این کامپوننت Bat بسته به پلتفرم، با استفاده از انگشت یا ماوس قابل کشیدن و رها کردن است. برای پیاده‌سازی این قابلیت، باید Mixin مربوط DragCallbacks را اضافه کنید و رویداد onDragUpdate را بازنویسی کنید.

در نهایت، کامپوننت Bat باید به کنترل صفحه کلید پاسخ دهد. تابع moveBy به کدهای دیگر اجازه می‌دهد تا به این خفاش بگویند که به اندازه تعداد مشخصی پیکسل مجازی به چپ یا راست حرکت کند. این تابع قابلیت جدیدی را در موتور بازی Flame معرفی می‌کند: Effect . با اضافه کردن شیء MoveToEffect به عنوان فرزند این کامپوننت، بازیکن خفاش را در موقعیت جدید متحرک می‌بیند. مجموعه‌ای از Effect ها در Flame برای اجرای جلوه‌های متنوع موجود است.

آرگومان‌های سازنده‌ی Effect شامل ارجاعی به دریافت‌کننده‌ی game است. به همین دلیل است که شما mixin مربوط به HasGameReference را در این کلاس قرار می‌دهید. این mixin یک دسترسی‌دهنده‌ی game از نوع ایمن (type-safe) به این کامپوننت اضافه می‌کند تا به نمونه‌ی BrickBreaker در بالای درخت کامپوننت دسترسی پیدا کند.

  1. برای اینکه Bat در دسترس BrickBreaker قرار گیرد، فایل lib/src/components/components.dart را به صورت زیر به‌روزرسانی کنید.

lib/src/components/components.dart

export 'ball.dart';
export 'bat.dart';                                              // Add this export
export 'play_area.dart';

خفاش را به جهان اضافه کنید

برای افزودن مؤلفه Bat به دنیای بازی، BrickBreaker به شرح زیر به‌روزرسانی کنید.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/events.dart';                             // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart';                         // And this import
import 'package:flutter/services.dart';                         // And this

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents {                // Modify this line
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(
      Ball(
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2(
          (rand.nextDouble() - 0.5) * width,
          height * 0.2,
        ).normalized()..scale(height / 4),
      ),
    );

    world.add(                                                  // Add from here...
      Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95),
      ),
    );                                                          // To here.

    debugMode = true;
  }

  @override                                                     // Add from here...
  KeyEventResult onKeyEvent(
    KeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
    }
    return KeyEventResult.handled;
  }                                                             // To here.
}

اضافه شدن mixin مربوط به KeyboardEvents و متد override شده‌ی onKeyEvent ورودی کیبورد را مدیریت می‌کند. کدی را که قبلاً برای حرکت دادن چوب بیسبال به اندازه‌ی گام مناسب اضافه کردید، به یاد بیاورید.

بخش باقیمانده از کد اضافه شده، چوب بیسبال را در موقعیت مناسب و با نسبت‌های صحیح به دنیای بازی اضافه می‌کند. قرار دادن تمام این تنظیمات در این فایل، توانایی شما را در تنظیم اندازه نسبی چوب بیسبال و توپ برای رسیدن به حس مناسب در بازی ساده می‌کند.

اگر در این مرحله بازی را انجام دهید، می‌بینید که می‌توانید چوب بیسبال را برای رهگیری توپ حرکت دهید، اما هیچ پاسخ قابل مشاهده‌ای دریافت نمی‌کنید، به جز گزارش اشکال‌زدایی که در کد تشخیص برخورد Ball باقی گذاشته‌اید.

وقتشه که این مشکل رو حل کنیم. کامپوننت Ball رو به صورت زیر ویرایش کنید.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';                           // Add this import
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import 'bat.dart';                                             // And this import
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
         radius: radius,
         anchor: Anchor.center,
         paint: Paint()
           ..color = const Color(0xff1e6091)
           ..style = PaintingStyle.fill,
         children: [CircleHitbox()],
       );

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(delay: 0.35));                         // Modify from here...
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x =
          velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else {                                                    // To here.
      debugPrint('collision with $other');
    }
  }
}

این تغییرات کد دو مشکل جداگانه را برطرف می‌کند.

اول، مشکل از بین رفتن توپ به محض برخورد با پایین صفحه را حل می‌کند. برای رفع این مشکل، فراخوانی removeFromParent را با RemoveEffect جایگزین می‌کنید. RemoveEffect توپ را پس از خروج از منطقه بازی قابل مشاهده، از دنیای بازی حذف می‌کند.

دوم، این تغییرات، نحوه برخورد بین چوب بیسبال و توپ را اصلاح می‌کند. این کد کنترل برخورد، بسیار به نفع بازیکن عمل می‌کند. تا زمانی که بازیکن توپ را با چوب بیسبال لمس کند، توپ به بالای صفحه برمی‌گردد. اگر این مورد بیش از حد آسان به نظر می‌رسد و شما چیزی واقع‌گرایانه‌تر می‌خواهید، این کنترل را تغییر دهید تا با حس و حال بازی شما مطابقت بیشتری داشته باشد.

شایان ذکر است که به پیچیدگی به‌روزرسانی velocity اشاره کنیم. این به‌روزرسانی فقط مؤلفه y سرعت را معکوس نمی‌کند، همانطور که برای برخورد با دیوار انجام شد. همچنین مؤلفه x را به روشی به‌روزرسانی می‌کند که به موقعیت نسبی چوب بیسبال و توپ در زمان تماس بستگی دارد. این به بازیکن کنترل بیشتری بر عملکرد توپ می‌دهد، اما نحوه دقیق آن به هیچ وجه به جز از طریق بازی به بازیکن منتقل نمی‌شود.

حالا که چوب بیسبال دارید که با آن به توپ ضربه بزنید، خیلی خوب می‌شود اگر چند آجر هم داشته باشید که با توپ بشکنید!

۸. دیوار را فرو بریزید

آجرها را ایجاد کنید

برای اضافه کردن آجر به بازی،

  1. تعدادی ثابت را به صورت زیر در فایل lib/src/config.dart وارد کنید.

lib/src/config.dart

import 'package:flutter/material.dart';                         // Add this import

const brickColors = [                                           // Add this const
  Color(0xfff94144),
  Color(0xfff3722c),
  Color(0xfff8961e),
  Color(0xfff9844a),
  Color(0xfff9c74f),
  Color(0xff90be6d),
  Color(0xff43aa8b),
  Color(0xff4d908e),
  Color(0xff277da1),
  Color(0xff577590),
];

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015;                          // Add from here...
final brickWidth =
    (gameWidth - (brickGutter * (brickColors.length + 1))) / brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03;                                // To here.
  1. کامپوننت Brick را به صورت زیر وارد کنید.

lib/src/components/brick.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
    : super(
        size: Vector2(brickWidth, brickHeight),
        anchor: Anchor.center,
        paint: Paint()
          ..color = color
          ..style = PaintingStyle.fill,
        children: [RectangleHitbox()],
      );

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();

    if (game.world.children.query<Brick>().length == 1) {
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

تا الان، بیشتر این کد باید برایتان آشنا باشد. این کد از یک RectangleComponent استفاده می‌کند که هم تشخیص برخورد و هم ارجاع ایمن از نوع به بازی BrickBreaker در بالای درخت کامپوننت را دارد.

مهم‌ترین مفهوم جدیدی که این کد معرفی می‌کند، نحوه‌ی دستیابی بازیکن به شرط برد است. بررسی شرط برد، از جهان درخواست آجر می‌کند و تأیید می‌کند که فقط یکی باقی مانده است. این ممکن است کمی گیج‌کننده باشد، زیرا خط قبلی این آجر را از والدش حذف می‌کند.

نکته کلیدی که باید درک کنید این است که حذف کامپوننت یک دستور صف‌بندی شده است. این دستور، آجر را پس از اجرای این کد، اما قبل از تیک بعدی دنیای بازی، حذف می‌کند.

برای اینکه کامپوننت Brick برای BrickBreaker قابل دسترسی باشد، lib/src/components/components.dart را به صورت زیر ویرایش کنید.

lib/src/components/components.dart

export 'ball.dart';
export 'bat.dart';
export 'brick.dart';                                            // Add this export
export 'play_area.dart';

آجرهایی به جهان اضافه کنید

کامپوننت Ball را به صورت زیر به‌روزرسانی کنید.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';                                            // Add this import
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
    required this.difficultyModifier,                           // Add this parameter
  }) : super(
         radius: radius,
         anchor: Anchor.center,
         paint: Paint()
           ..color = const Color(0xff1e6091)
           ..style = PaintingStyle.fill,
         children: [CircleHitbox()],
       );

  final Vector2 velocity;
  final double difficultyModifier;                              // Add this member

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(delay: 0.35));
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x =
          velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else if (other is Brick) {                                // Modify from here...
      if (position.y < other.position.y - other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.y > other.position.y + other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.x < other.position.x) {
        velocity.x = -velocity.x;
      } else if (position.x > other.position.x) {
        velocity.x = -velocity.x;
      }
      velocity.setFrom(velocity * difficultyModifier);          // To here.
    }
  }
}

این تنها جنبه جدید را معرفی می‌کند، یک اصلاح‌کننده سختی که سرعت توپ را پس از هر برخورد آجر افزایش می‌دهد. این پارامتر قابل تنظیم باید آزمایش شود تا منحنی سختی مناسب برای بازی شما پیدا شود.

بازی BrickBreaker را به صورت زیر ویرایش کنید.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

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

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents {
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(
      Ball(
        difficultyModifier: difficultyModifier,                 // Add this argument
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2(
          (rand.nextDouble() - 0.5) * width,
          height * 0.2,
        ).normalized()..scale(height / 4),
      ),
    );

    world.add(
      Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95),
      ),
    );

    await world.addAll([                                        // Add from here...
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);                                                         // To here.

    debugMode = true;
  }

  @override
  KeyEventResult onKeyEvent(
    KeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
    }
    return KeyEventResult.handled;
  }
}

اگر بازی را اجرا کنید، تمام مکانیک‌های کلیدی بازی نمایش داده می‌شود. می‌توانید اشکال‌زدایی را غیرفعال کنید و آن را تمام‌شده بنامید، اما احساس می‌کنید چیزی کم است.

تصویری از بازی brick_breaker به همراه توپ، چوب بیسبال و بیشتر آجرهای روی زمین بازی. هر یک از اجزا دارای برچسب‌های اشکال‌زدایی هستند.

نظرتان در مورد یک صفحه خوشامدگویی، یک صفحه پایان بازی و شاید یک امتیاز چیست؟ فلاتر می‌تواند این ویژگی‌ها را به بازی اضافه کند و اینجاست که توجه شما را جلب خواهد کرد.

۹. بازی را ببرید

حالت‌های پخش را اضافه کنید

در این مرحله، بازی Flame را درون یک پوشش Flutter جاسازی می‌کنید و سپس پوشش‌های Flutter را برای صفحات خوشامدگویی، پایان بازی و برنده شدن اضافه می‌کنید.

ابتدا، فایل‌های بازی و کامپوننت را تغییر می‌دهید تا حالتی برای اجرا پیاده‌سازی شود که نشان دهد آیا یک پوشش نمایش داده شود یا خیر، و اگر چنین است، کدام یک.

  1. بازی BrickBreaker را به صورت زیر تغییر دهید.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

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

import 'components/components.dart';
import 'config.dart';

enum PlayState { welcome, playing, gameOver, won }              // Add this enumeration

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents, TapDetector {   // Modify this line
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  late PlayState _playState;                                    // Add from here...
  PlayState get playState => _playState;
  set playState(PlayState playState) {
    _playState = playState;
    switch (playState) {
      case PlayState.welcome:
      case PlayState.gameOver:
      case PlayState.won:
        overlays.add(playState.name);
      case PlayState.playing:
        overlays.remove(PlayState.welcome.name);
        overlays.remove(PlayState.gameOver.name);
        overlays.remove(PlayState.won.name);
    }
  }                                                             // To here.

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    playState = PlayState.welcome;                              // Add from here...
  }

  void startGame() {
    if (playState == PlayState.playing) return;

    world.removeAll(world.children.query<Ball>());
    world.removeAll(world.children.query<Bat>());
    world.removeAll(world.children.query<Brick>());

    playState = PlayState.playing;                              // To here.

    world.add(
      Ball(
        difficultyModifier: difficultyModifier,
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2(
          (rand.nextDouble() - 0.5) * width,
          height * 0.2,
        ).normalized()..scale(height / 4),
      ),
    );

    world.add(
      Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95),
      ),
    );

    world.addAll([                                              // Drop the await
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);
  }                                                             // Drop the debugMode

  @override                                                     // Add from here...
  void onTap() {
    super.onTap();
    startGame();
  }                                                             // To here.

  @override
  KeyEventResult onKeyEvent(
    KeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
      case LogicalKeyboardKey.space:                            // Add from here...
      case LogicalKeyboardKey.enter:
        startGame();                                            // To here.
    }
    return KeyEventResult.handled;
  }

  @override
  Color backgroundColor() => const Color(0xfff2e8cf);          // Add this override
}

این کد بخش زیادی از بازی BrickBreaker تغییر می‌دهد. اضافه کردن شمارش playState کار زیادی می‌برد. این شمارش، موقعیت بازیکن را در هنگام ورود، بازی و باخت یا برد بازی ثبت می‌کند. در بالای فایل، شمارش را تعریف می‌کنید، سپس آن را به عنوان یک حالت پنهان با getterها و setterهای منطبق، نمونه‌سازی می‌کنید. این getterها و setterها امکان اصلاح overlayها را در زمانی که بخش‌های مختلف بازی باعث تغییر حالت‌های بازی می‌شوند، فراهم می‌کنند.

در مرحله بعد، کد موجود در onLoad را به onLoad و یک متد جدید startGame تقسیم می‌کنید. قبل از این تغییر، فقط می‌توانستید با راه‌اندازی مجدد بازی، یک بازی جدید را شروع کنید. با این اضافات جدید، بازیکن اکنون می‌تواند بدون چنین اقدامات شدیدی، یک بازی جدید را شروع کند.

برای اینکه به بازیکن اجازه دهید یک بازی جدید را شروع کند، دو کنترل‌کننده جدید برای بازی پیکربندی کرده‌اید. یک کنترل‌کننده ضربه اضافه کرده‌اید و کنترل‌کننده صفحه‌کلید را گسترش داده‌اید تا کاربر بتواند یک بازی جدید را در چندین حالت شروع کند. با مدل‌سازی حالت بازی، منطقی است که اجزا را به‌روزرسانی کنید تا هنگام برد یا باخت بازیکن، انتقال حالت بازی را فعال کنند.

  1. کامپوننت Ball را به صورت زیر تغییر دهید.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
    required this.difficultyModifier,
  }) : super(
         radius: radius,
         anchor: Anchor.center,
         paint: Paint()
           ..color = const Color(0xff1e6091)
           ..style = PaintingStyle.fill,
         children: [CircleHitbox()],
       );

  final Vector2 velocity;
  final double difficultyModifier;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(
          RemoveEffect(
            delay: 0.35,
            onComplete: () {                                    // Modify from here
              game.playState = PlayState.gameOver;
            },
          ),
        );                                                      // To here.
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x =
          velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else if (other is Brick) {
      if (position.y < other.position.y - other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.y > other.position.y + other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.x < other.position.x) {
        velocity.x = -velocity.x;
      } else if (position.x > other.position.x) {
        velocity.x = -velocity.x;
      }
      velocity.setFrom(velocity * difficultyModifier);
    }
  }
}

این تغییر کوچک یک فراخوانی onComplete به RemoveEffect اضافه می‌کند که حالت بازی gameOver را فعال می‌کند. اگر بازیکن اجازه دهد توپ از پایین صفحه فرار کند، این باید تقریباً درست به نظر برسد.

  1. کامپوننت Brick را به صورت زیر ویرایش کنید.

lib/src/components/brick.dart

impimport 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
    : super(
        size: Vector2(brickWidth, brickHeight),
        anchor: Anchor.center,
        paint: Paint()
          ..color = color
          ..style = PaintingStyle.fill,
        children: [RectangleHitbox()],
      );

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();

    if (game.world.children.query<Brick>().length == 1) {
      game.playState = PlayState.won;                          // Add this line
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

از طرف دیگر، اگر بازیکن بتواند تمام آجرها را بشکند، صفحه "برد بازی" را به دست آورده است. آفرین بازیکن، آفرین!

اضافه کردن بسته‌بندی Flutter

برای فراهم کردن جایی برای جاسازی بازی و افزودن پوشش‌های حالت بازی، پوسته Flutter را اضافه کنید.

  1. یک پوشه‌ی widgets در مسیر lib/src ایجاد کنید.
  2. یک فایل game_app.dart اضافه کنید و محتوای زیر را در آن فایل قرار دهید.

lib/src/widgets/game_app.dart

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

import '../brick_breaker.dart';
import '../config.dart';

class GameApp extends StatelessWidget {
  const GameApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [Color(0xffa9d6e5), Color(0xfff2e8cf)],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: FittedBox(
                  child: SizedBox(
                    width: gameWidth,
                    height: gameHeight,
                    child: GameWidget.controlled(
                      gameFactory: BrickBreaker.new,
                      overlayBuilderMap: {
                        PlayState.welcome.name: (context, game) => Center(
                          child: Text(
                            'TAP TO PLAY',
                            style: Theme.of(context).textTheme.headlineLarge,
                          ),
                        ),
                        PlayState.gameOver.name: (context, game) => Center(
                          child: Text(
                            'G A M E   O V E R',
                            style: Theme.of(context).textTheme.headlineLarge,
                          ),
                        ),
                        PlayState.won.name: (context, game) => Center(
                          child: Text(
                            'Y O U   W O N ! ! !',
                            style: Theme.of(context).textTheme.headlineLarge,
                          ),
                        ),
                      },
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

بیشتر محتوای این فایل از ساختار درختی ویجت استاندارد Flutter پیروی می‌کند. بخش‌های مختص Flame شامل استفاده از GameWidget.controlled برای ساخت و مدیریت نمونه بازی BrickBreaker و آرگومان جدید overlayBuilderMap به GameWidget است.

کلیدهای این overlayBuilderMap باید با overlayهایی که تنظیم‌کننده playState در BrickBreaker اضافه یا حذف کرده است، همسو باشند. تلاش برای تنظیم overlay که در این نقشه نیست، منجر به چهره‌های ناراضی در اطراف می‌شود.

  1. برای دریافت این قابلیت جدید روی صفحه، فایل lib/main.dart را با محتوای زیر جایگزین کنید.

lib/main.dart

import 'package:flutter/material.dart';

import 'src/widgets/game_app.dart';

void main() {
  runApp(const GameApp());
}

اگر این کد را روی iOS، لینوکس، ویندوز یا وب اجرا کنید، خروجی مورد نظر در بازی نمایش داده می‌شود. اگر macOS یا اندروید را هدف قرار داده‌اید، برای فعال کردن نمایش google_fonts به یک تغییر دیگر نیاز دارید.

فعال کردن دسترسی به فونت

اضافه کردن دسترسی به اینترنت برای اندروید

برای اندروید، باید مجوز اینترنت را اضافه کنید. AndroidManifest.xml خود را به صورت زیر ویرایش کنید.

اندروید/برنامه/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Add the following line -->
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:label="brick_breaker"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

ویرایش فایل‌های عنوان‌بندی برای macOS

برای macOS، شما دو فایل برای ویرایش دارید.

  1. فایل DebugProfile.entitlements را ویرایش کنید تا با کد زیر مطابقت داشته باشد.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- to here. -->
</dict>
</plist>
  1. فایل Release.entitlements را ویرایش کنید تا با کد زیر مطابقت داشته باشد.

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- to here. -->
</dict>
</plist>

اجرای این کد به همین شکل باید یک صفحه خوشامدگویی و یک صفحه «بازی تمام شد» یا «برنده شد» را در همه پلتفرم‌ها نمایش دهد. این صفحه‌ها ممکن است کمی ساده باشند و خوب است که امتیاز داشته باشید. بنابراین، حدس بزنید که در مرحله بعدی چه کاری انجام خواهید داد!

۱۰. امتیاز بگیرید

اضافه کردن امتیاز به بازی

در این مرحله، امتیاز بازی را در اختیار محیط اطراف Flutter قرار می‌دهید. در این مرحله، وضعیت بازی Flame را در اختیار مدیریت وضعیت Flutter قرار می‌دهید. این کار به کد بازی این امکان را می‌دهد که هر بار که بازیکن آجری را می‌شکند، امتیاز را به‌روزرسانی کند.

  1. بازی BrickBreaker را به صورت زیر تغییر دهید.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

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

import 'components/components.dart';
import 'config.dart';

enum PlayState { welcome, playing, gameOver, won }

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents, TapDetector {
  BrickBreaker()
    : super(
        camera: CameraComponent.withFixedResolution(
          width: gameWidth,
          height: gameHeight,
        ),
      );

  final ValueNotifier<int> score = ValueNotifier(0);            // Add this line
  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  late PlayState _playState;
  PlayState get playState => _playState;
  set playState(PlayState playState) {
    _playState = playState;
    switch (playState) {
      case PlayState.welcome:
      case PlayState.gameOver:
      case PlayState.won:
        overlays.add(playState.name);
      case PlayState.playing:
        overlays.remove(PlayState.welcome.name);
        overlays.remove(PlayState.gameOver.name);
        overlays.remove(PlayState.won.name);
    }
  }

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    playState = PlayState.welcome;
  }

  void startGame() {
    if (playState == PlayState.playing) return;

    world.removeAll(world.children.query<Ball>());
    world.removeAll(world.children.query<Bat>());
    world.removeAll(world.children.query<Brick>());

    playState = PlayState.playing;
    score.value = 0;                                            // Add this line

    world.add(
      Ball(
        difficultyModifier: difficultyModifier,
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2(
          (rand.nextDouble() - 0.5) * width,
          height * 0.2,
        ).normalized()..scale(height / 4),
      ),
    );

    world.add(
      Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95),
      ),
    );

    world.addAll([
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);
  }

  @override
  void onTap() {
    super.onTap();
    startGame();
  }

  @override
  KeyEventResult onKeyEvent(
    KeyEvent event,
    Set<LogicalKeyboardKey> keysPressed,
  ) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
      case LogicalKeyboardKey.space:
      case LogicalKeyboardKey.enter:
        startGame();
    }
    return KeyEventResult.handled;
  }

  @override
  Color backgroundColor() => const Color(0xfff2e8cf);
}

با اضافه کردن score به بازی، وضعیت بازی را به مدیریت وضعیت فلاتر (Flutter) گره می‌زنید.

  1. کلاس Brick را طوری تغییر دهید که وقتی بازیکن آجرها را می‌شکند، یک امتیاز به امتیازش اضافه شود.

lib/src/components/brick.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
    : super(
        size: Vector2(brickWidth, brickHeight),
        anchor: Anchor.center,
        paint: Paint()
          ..color = color
          ..style = PaintingStyle.fill,
        children: [RectangleHitbox()],
      );

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();
    game.score.value++;                                         // Add this line

    if (game.world.children.query<Brick>().length == 1) {
      game.playState = PlayState.won;
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

یک بازی زیبا بسازید

حالا که می‌توانید امتیاز خود را در فلاتر ثبت کنید، وقت آن رسیده که ویجت‌ها را کنار هم قرار دهید تا ظاهر خوبی داشته باشند.

  1. Create score_card.dart in lib/src/widgets and add the following.

lib/src/widgets/score_card.dart

import 'package:flutter/material.dart';

class ScoreCard extends StatelessWidget {
  const ScoreCard({super.key, required this.score});

  final ValueNotifier<int> score;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<int>(
      valueListenable: score,
      builder: (context, score, child) {
        return Padding(
          padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
          child: Text(
            'Score: $score'.toUpperCase(),
            style: Theme.of(context).textTheme.titleLarge!,
          ),
        );
      },
    );
  }
}
  1. Create overlay_screen.dart in lib/src/widgets and add the following code.

This adds more polish to the overlays using the power of the flutter_animate package to add some movement and style to the overlay screens.

lib/src/widgets/overlay_screen.dart

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

class OverlayScreen extends StatelessWidget {
  const OverlayScreen({super.key, required this.title, required this.subtitle});

  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: const Alignment(0, -0.15),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            title,
            style: Theme.of(context).textTheme.headlineLarge,
          ).animate().slideY(duration: 750.ms, begin: -3, end: 0),
          const SizedBox(height: 16),
          Text(subtitle, style: Theme.of(context).textTheme.headlineSmall)
              .animate(onPlay: (controller) => controller.repeat())
              .fadeIn(duration: 1.seconds)
              .then()
              .fadeOut(duration: 1.seconds),
        ],
      ),
    );
  }
}

To get a more in-depth look at the power of flutter_animate , check out the Building next generation UIs in Flutter codelab.

This code changed a lot in the GameApp component. First, to enable ScoreCard to access the score , you convert it from a StatelessWidget to StatefulWidget . The addition of the score card requires the addition of a Column to stack the score above the game.

Second, to enhance the welcome, game over, and won experiences, you added the new OverlayScreen widget.

lib/src/widgets/game_app.dart

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

import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart';                                   // Add this import
import 'score_card.dart';                                       // And this one too

class GameApp extends StatefulWidget {                          // Modify this line
  const GameApp({super.key});

  @override                                                     // Add from here...
  State<GameApp> createState() => _GameAppState();
}

class _GameAppState extends State<GameApp> {
  late final BrickBreaker game;

  @override
  void initState() {
    super.initState();
    game = BrickBreaker();
  }                                                             // To here.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [Color(0xffa9d6e5), Color(0xfff2e8cf)],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: Column(                                  // Modify from here...
                  children: [
                    ScoreCard(score: game.score),
                    Expanded(
                      child: FittedBox(
                        child: SizedBox(
                          width: gameWidth,
                          height: gameHeight,
                          child: GameWidget(
                            game: game,
                            overlayBuilderMap: {
                              PlayState.welcome.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'TAP TO PLAY',
                                    subtitle: 'Use arrow keys or swipe',
                                  ),
                              PlayState.gameOver.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'G A M E   O V E R',
                                    subtitle: 'Tap to Play Again',
                                  ),
                              PlayState.won.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'Y O U   W O N ! ! !',
                                    subtitle: 'Tap to Play Again',
                                  ),
                            },
                          ),
                        ),
                      ),
                    ),
                  ],
                ),                                              // To here.
              ),
            ),
          ),
        ),
      ),
    );
  }
}

With that all in place, you should now be able to run this game on any of the six Flutter target platforms. The game should resemble the following.

A screenshot of brick_breaker showing the pre-game screen inviting the user to tap the screen to play the game

A screenshot of brick_breaker showing the game over screen overlaid on top of a bat and some of the bricks

۱۱. تبریک

Congratulations, you succeeded in building a game with Flutter and Flame!

You built a game using the Flame 2D game engine and embedded it in a Flutter wrapper. You used Flame's Effects to animate and remove components. You used Google Fonts and Flutter Animate packages to make the whole game look well designed.

بعدش چی؟

Check out some of these codelabs...

مطالعه بیشتر