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

1. مقدمه

Flame یک موتور بازی دو بعدی مبتنی بر فلاتر است. در این کد لبه، شما یک بازی با الهام از یکی از کلاسیک های بازی های ویدیویی دهه 70، Breakout استیو وزنیاک خواهید ساخت. شما از اجزای Flame's Components برای کشیدن خفاش، توپ و آجر استفاده خواهید کرد. شما از افکت های Flame برای متحرک سازی حرکت خفاش استفاده خواهید کرد و نحوه ادغام Flame با سیستم مدیریت ایالت فلاتر را مشاهده خواهید کرد.

پس از اتمام، بازی شما باید مانند این گیف متحرک به نظر برسد، البته کمی کندتر.

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

چیزی که یاد خواهید گرفت

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

چیزی که خواهی ساخت

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

  • عملکرد بر روی هر شش پلت فرمی که Flutter پشتیبانی می کند: Android، iOS، Linux، macOS، Windows و وب
  • با استفاده از حلقه بازی Flame حداقل 60 فریم در ثانیه را حفظ کنید.
  • از قابلیت‌های Flutter مانند بسته google_fonts و flutter_animate برای بازسازی حس بازی‌های آرکید دهه 80 استفاده کنید.

2. محیط Flutter خود را تنظیم کنید

ویرایشگر

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

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

تصویری از VS Code با مقداری کد فلاتر

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

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

  • iOS
  • اندروید
  • ویندوز
  • macOS
  • لینوکس
  • وب

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

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

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

ممکن است وسوسه شوید که وب را به عنوان هدف توسعه خود انتخاب کنید. این یک جنبه منفی در طول توسعه دارد: شما قابلیت بارگذاری مجدد داغ Flutter Stateful را از دست می دهید. Flutter در حال حاضر نمی‌تواند برنامه‌های وب را دوباره بارگیری کند.

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

فلاتر را نصب کنید

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

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

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

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

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

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

3. یک پروژه ایجاد کنید

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

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

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

اسکرین شات VS Code با

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

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

  1. نام پروژه خود را brick_breaker بگذارید. بقیه این کد لبه فرض می کند که نام برنامه خود را brick_breaker گذاشته اید.

اسکرین شات از VS Code با

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

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

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

  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.3.0 <4.0.0'

dependencies:
  flame: ^1.16.0
  flutter:
    sdk: flutter
  flutter_animate: ^4.5.0
  google_fonts: ^6.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.1

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. این کد را اجرا کنید تا مطمئن شوید همه چیز کار می کند. باید یک پنجره جدید فقط با یک پس زمینه سیاه خالی نمایش دهد. بدترین بازی ویدیویی جهان اکنون با سرعت 60 فریم بر ثانیه رندر می شود!

یک عکس از صفحه نمایش یک پنجره برنامه brick_breaker را نشان می دهد که کاملا سیاه است.

4. بازی را ایجاد کنید

اندازه بازی را افزایش دهید

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

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

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

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

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;

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

یک PlayArea ایجاد کنید

در بازی 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);
  }
}

جایی که Flutter دارای Widget s است، Flame دارای Component است. در جایی که برنامه‌های Flutter از ایجاد درخت‌های ویجت‌ها تشکیل شده‌اند، بازی‌های Flame شامل نگهداری درخت‌های اجزا هستند.

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

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

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

lib/src/components/components.dart

export 'play_area.dart';

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

یک بازی Flame ایجاد کنید

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

  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 بتوانند خود را به اندازه مناسب تنظیم کنند.

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

  1. بالا سمت چپ را به عنوان لنگر برای منظره یاب پیکربندی می کند. به طور پیش فرض، منظره یاب از وسط ناحیه به عنوان لنگر برای (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 را با یک مستطیل شنی رنگ در وسط پنجره برنامه نشان می دهد.

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

5. توپ را نمایش دهید

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

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

  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 یا تغییر موقعیت در طول زمان را معرفی می کند. 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 اصلی تنظیم شده است، اما به فاصله 1 کاهش می یابد. این کار سرعت توپ را بدون توجه به جهتی که توپ می رود ثابت نگه می دارد. سپس سرعت توپ به اندازه 1/4 ارتفاع بازی افزایش می یابد.

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

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

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

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

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

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

6. به اطراف بپرید

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

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

برای افزودن تشخیص برخورد به بازی، میکس 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;
  }
}

این هیت باکس اجزا را ردیابی می‌کند و در هر تیک بازی، تماس‌های برگشتی ایجاد می‌کند.

برای شروع پر کردن هیت‌باکس‌های بازی، مؤلفه 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 ، یک جعبه ضربه برای تشخیص برخورد ایجاد می کند که با اندازه مؤلفه والد مطابقت دارد. یک سازنده کارخانه برای 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.
}

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

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

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

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

7. ضربه زدن به توپ

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

برای اضافه کردن یک چوب برای حفظ توپ در بازی،

  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 callback را لغو می کند.

با دقت به فراخوان canvas.drawRRect (مستطیل گرد بکشید)، و ممکن است از خود بپرسید، "مستطیل کجاست؟" Offset.zero & size.toSize() از یک operator & اضافه بار در کلاس dart:ui Offset استفاده می کند که Rect s را ایجاد می کند. این مختصر ممکن است در ابتدا شما را گیج کند، اما اغلب آن را در کدهای فلاتر و شعله سطح پایین تر مشاهده خواهید کرد.

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

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

آرگومان های سازنده The Effect شامل ارجاع به گیرنده game است. به همین دلیل است که میکس HasGameReference را در این کلاس قرار می دهید. این میکسین برای دسترسی به نمونه BrickBreaker در بالای درخت کامپوننت، یک ابزار دسترسی ایمن game به این کامپوننت اضافه می کند.

  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(Bat(                                              // Add from here...
        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
}

افزودن ترکیب KeyboardEvents و روش لغو شده 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(                                       // Modify from here...
          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 {                                                    // To here.
      debugPrint('collision with $other');
    }
  }
}

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

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

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

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

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

8. دیوار را خراب کنید

ایجاد آجر

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

  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 را با توپ، خفاش و بیشتر آجرهای محوطه بازی نشان می‌دهد. هر یک از مؤلفه ها دارای برچسب های اشکال زدایی هستند

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

9. بازی را برنده شوید

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

در این مرحله، بازی 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 }              // 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 کار زیادی می طلبد. این نشان می دهد که بازیکن در حال ورود، بازی، و باخت یا برنده شدن در بازی است. در بالای فایل، شمارش را تعریف می‌کنید، سپس آن را به‌عنوان یک حالت پنهان با دریافت‌کننده‌ها و تنظیم‌کننده‌های منطبق، نمونه‌سازی می‌کنید. این گیرنده‌ها و تنظیم‌کننده‌ها امکان اصلاح همپوشانی‌ها را هنگامی که بخش‌های مختلف بازی باعث انتقال حالت بازی می‌شوند را ممکن می‌سازد.

سپس کد موجود در 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

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.playState = PlayState.won;                          // Add this line
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

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

فلاتر را اضافه کنید

برای ارائه مکانی برای جاسازی بازی و افزودن همپوشانی حالت بازی، پوسته 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(
        useMaterial3: true,
        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 باید با هم‌پوشانی‌هایی که تنظیم‌کننده playState در BrickBreaker اضافه یا حذف کرده‌اند، هماهنگ باشند. تلاش برای تنظیم پوششی که در این نقشه وجود ندارد منجر به چهره‌های ناراضی در اطراف می‌شود.

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

lib/main.dart

import 'package:flutter/material.dart';

import 'src/widgets/game_app.dart';

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

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

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

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

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

android/app/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>

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

10. امتیاز را حفظ کنید

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

در این مرحله، امتیاز بازی را در معرض زمینه فلاتر اطراف قرار می دهید. در این مرحله شما حالت را از بازی 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 به بازی، وضعیت بازی را به مدیریت حالت فلاتر گره می‌زنید.

  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. score_card.dart را در lib/src/widgets ایجاد کنید و موارد زیر را اضافه کنید.

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. overlay_screen.dart را در lib/src/widgets ایجاد کنید و کد زیر را اضافه کنید.

این امر با استفاده از قدرت بسته flutter_animate ، به پوشش های بیشتری به پوشش می دهد تا حرکتی و سبک را به صفحه های روکش اضافه کند.

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

برای به دست آوردن نگاهی عمیق تر به قدرت flutter_animate ، ساختمان بعدی UIS را در Flutter CodeLab بررسی کنید.

این کد در مؤلفه GameApp بسیار تغییر کرد. اول ، برای دسترسی به ScoreCard برای دسترسی به score ، آن را از یک StatelessWidget به StatefulWidget تبدیل می کنید. علاوه بر این کارت امتیاز ، نیاز به اضافه کردن یک Column برای جمع آوری نمره بالاتر از بازی دارد.

دوم ، برای تقویت استقبال ، بازی و تجربه های خود ، ویجت OverlayScreen جدید را اضافه کردید.

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(
        useMaterial3: true,
        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.
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

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

یک عکس صفحه از Brick_breaker که بازی را روی صفحه نمایش در بالای یک خفاش و برخی از آجر نشان می دهد

11. تبریک می گویم

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

شما یک بازی را با استفاده از موتور بازی 2D Flame 2D ساخته و آن را در یک بسته بندی فلوتر تعبیه کرده اید. شما از جلوه های شعله برای تحریک و حذف مؤلفه ها استفاده کردید. شما از فونت های Google و بسته های متحرک Flutter استفاده کرده اید تا کل بازی به خوبی طراحی شده باشد.

بعدش چی؟

برخی از این codelabs را بررسی کنید ...

در ادامه مطلب