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

آنچه یاد خواهید گرفت
- نحوه کار اصول اولیه Flame، با شروع از
GameWidget. - نحوه استفاده از حلقه بازی.
- نحوه کار
ComponentFlame. آنها شبیه بهWidgetFlutter هستند. - نحوه برخورد با تصادمها.
- نحوه استفاده از
Effectها برای متحرک سازیComponentها. - نحوهی قرار دادن
WidgetFlutter روی یک بازی 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 کار میکنند.

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

برای مثال: فرض کنید از یک لپتاپ ویندوزی برای توسعه اپلیکیشن Flutter خود استفاده میکنید. سپس اندروید را به عنوان هدف توسعه خود انتخاب میکنید. برای پیشنمایش اپلیکیشن خود، یک دستگاه اندروید را با کابل USB به لپتاپ ویندوزی خود متصل میکنید و اپلیکیشن در حال توسعه شما روی آن دستگاه اندروید متصل یا در یک شبیهساز اندروید اجرا میشود. میتوانستید ویندوز را به عنوان هدف توسعه انتخاب کنید که اپلیکیشن در حال توسعه شما را به عنوان یک اپلیکیشن ویندوزی در کنار ویرایشگر شما اجرا میکند.
قبل از ادامه، انتخاب خود را انجام دهید. همیشه میتوانید برنامه خود را بعداً روی سیستمعاملهای دیگر اجرا کنید. انتخاب یک هدف توسعه، گام بعدی را هموارتر میکند.
نصب فلاتر
جدیدترین دستورالعملهای نصب SDK فلاتر را میتوانید در docs.flutter.dev پیدا کنید.
دستورالعملهای موجود در وبسایت Flutter شامل نصب SDK و ابزارهای مرتبط با هدف توسعه و افزونههای ویرایشگر است. برای این codelab، نرمافزارهای زیر را نصب کنید:
- کیت توسعه نرمافزار فلاتر
- ویژوال استودیو کد با افزونه فلاتر
- نرمافزار کامپایلر برای هدف توسعه انتخابی شما. (برای هدف قرار دادن ویندوز به Visual Studio و برای هدف قرار دادن macOS یا iOS به Xcode نیاز دارید)
در بخش بعدی، اولین پروژه فلاتر خود را ایجاد خواهید کرد.
اگر نیاز به عیبیابی هرگونه مشکلی دارید، ممکن است برخی از این پرسش و پاسخها (از StackOverflow) برای عیبیابی مفید باشند.
سوالات متداول
- چگونه مسیر SDK فلاتر را پیدا کنم؟
- وقتی دستور Flutter پیدا نشد، چه کاری باید انجام دهم؟
- چگونه میتوانم مشکل «منتظر دستور دیگری برای آزادسازی قفل راهاندازی هستم» را برطرف کنم؟
- چگونه به Flutter بگویم که محل نصب SDK اندروید من کجاست؟
- چگونه میتوانم با خطای جاوا هنگام اجرای
flutter doctor --android-licensesمقابله کنم؟ - چگونه با خطای «ابزار
sdkmanagerاندروید یافت نشد» (Android sdkmanager tool not found) برخورد کنم؟ - چگونه با خطای "
cmdline-toolscomponent is missing" مقابله کنم؟ - چگونه میتوانم CocoaPods را روی Apple Silicon (M1) اجرا کنم؟
- چگونه میتوانم قالببندی خودکار هنگام ذخیره در VS Code را غیرفعال کنم؟
۳. ایجاد یک پروژه
اولین پروژه فلاتر خود را ایجاد کنید
این شامل باز کردن VS Code و ایجاد الگوی برنامه Flutter در دایرکتوری مورد نظر شما میشود.
- ویژوال استودیو کد را اجرا کنید.
- پالت دستورات (
F1یاCtrl+Shift+PیاShift+Cmd+P) را باز کنید، سپس عبارت "flutter new" را تایپ کنید. وقتی ظاهر شد، دستور Flutter: New Project را انتخاب کنید.

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

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

فلاتر اکنون پوشه پروژه شما را ایجاد میکند و VS Code آن را باز میکند. اکنون محتویات دو فایل را با یک چارچوب اولیه از برنامه بازنویسی خواهید کرد.
برنامه اولیه را کپی و جایگذاری کنید
این کد نمونه ارائه شده در این codelab را به برنامه شما اضافه میکند.
- در پنل سمت چپ VS Code، روی Explorer کلیک کنید و فایل
pubspec.yamlرا باز کنید.

- محتویات این فایل را با موارد زیر جایگزین کنید:
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 اطلاعات اولیه در مورد برنامه شما، مانند نسخه فعلی آن، وابستگیهای آن و داراییهایی که با آنها ارسال خواهد شد را مشخص میکند.
- فایل
main.dartرا در دایرکتوریlib/باز کنید.

- محتویات این فایل را با موارد زیر جایگزین کنید:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
final game = FlameGame();
runApp(GameWidget(game: game));
}
- این کد را اجرا کنید تا مطمئن شوید همه چیز درست کار میکند. باید یک پنجره جدید با پسزمینه سیاه خالی نمایش داده شود. بدترین بازی ویدیویی دنیا اکنون با سرعت ۶۰ فریم در ثانیه رندر میشود!

۴. بازی را بسازید
بازی را بزرگ کنید
یک بازی که در دو بعد (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 نیاز دارید.
- فایلی به نام
play_area.dartدر دایرکتوری جدیدی به نامlib/src/componentsایجاد کنید. - موارد زیر را به این فایل اضافه کنید.
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 برای بیان مکانیک بازی بهینه شدهاند. این آزمایشگاه کد با حلقه بازی که در مرحله بعدی نمایش داده میشود، شروع خواهد شد.
- برای کنترل بینظمی، فایلی حاوی تمام کامپوننتهای این پروژه اضافه کنید. یک فایل
components.dartدرlib/src/componentsایجاد کنید و محتوای زیر را به آن اضافه کنید.
lib/src/components/components.dart
export 'play_area.dart';
دستور export نقش معکوس import را ایفا میکند. این دستور مشخص میکند که این فایل هنگام وارد شدن به فایل دیگر چه عملکردی را نشان میدهد. با اضافه شدن اجزای جدید در مراحل بعدی، ورودیهای این فایل افزایش مییابد.
یک بازی شعله ایجاد کنید
برای محو کردن خطوط موجدار قرمز از مرحله قبل، یک زیرکلاس جدید برای FlameGame از کلاس Flame مشتق کنید.
- یک فایل با نام
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 ، کد شما دو عمل انجام میدهد.
- بالا سمت چپ را به عنوان نقطه مرجع برای منظرهیاب تنظیم میکند. به طور پیشفرض،
viewfinderاز وسط ناحیه به عنوان نقطه مرجع برای(0,0)استفاده میکند. -
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));
}
بعد از انجام این تغییرات، بازی را مجدداً راه اندازی کنید. بازی باید شبیه شکل زیر باشد.

در مرحله بعد، یک توپ به دنیا اضافه میکنید و آن را به حرکت درمیآورید!
۵. توپ را به نمایش بگذارید
کامپوننت توپ را ایجاد کنید
قرار دادن یک توپ متحرک روی صفحه نمایش شامل ایجاد یک جزء دیگر و اضافه کردن آن به دنیای بازی است.
- محتویات فایل
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 سطح بالا را تغییر دهید تا ببینید که ظاهر و حس بازی در نتیجه چگونه تغییر میکند.
- کامپوننت
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 توجه ویژه داشته باشید. اینگونه است که شما بهروزرسانی یک شبیهسازی گسسته از حرکت را در طول زمان پیادهسازی میکنید.
- برای افزودن کامپوننت
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 اصلی تنظیم شده است، اما به فاصله ۱ کاهش یافته است. این کار سرعت توپ را صرف نظر از جهتی که توپ میرود، ثابت نگه میدارد. سپس سرعت توپ به ۱/۴ ارتفاع بازی افزایش مییابد.
درست بدست آوردن این مقادیر مختلف مستلزم مقداری تکرار است که در صنعت بازی به عنوان تست بازی نیز شناخته میشود.
خط آخر، نمایشگر اشکالزدایی را روشن میکند که اطلاعات اضافی را برای کمک به اشکالزدایی به نمایشگر اضافه میکند.
وقتی بازی را اجرا کنید، باید چیزی شبیه به تصویر زیر باشد.

هم کامپوننت 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 نیز اضافه میکند تا در صورت برخورد توپ با چیزهایی غیر از خفاش، آن را مدیریت کند. اگر مایل باشید، این یک یادآوری ملایم برای پیادهسازی منطق باقیمانده است.
وقتی توپ به دیوار پایینی برخورد میکند، در حالی که هنوز کاملاً در دید است، از سطح بازی ناپدید میشود. شما این اثر باستانی را در مرحله بعدی، با استفاده از قدرت جلوههای شعله، مدیریت خواهید کرد.
حالا که توپ به دیوارهای بازی برخورد میکند، مطمئناً مفید خواهد بود که به بازیکن یک چوب بیسبال بدهید تا با آن به توپ ضربه بزند...
۷. ضربه چوب بیسبال را به سمت توپ بگیرید
خفاش را ایجاد کنید
برای اضافه کردن چوب بیسبال برای نگه داشتن توپ در جریان بازی،
- تعدادی ثابت را به صورت زیر در فایل
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 میزان گامهای چوب بیسبال را برای هر بار فشردن کلید جهت چپ یا راست تنظیم میکند.
- کلاس کامپوننت
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 در بالای درخت کامپوننت دسترسی پیدا کند.
- برای اینکه
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 را به روشی بهروزرسانی میکند که به موقعیت نسبی چوب بیسبال و توپ در زمان تماس بستگی دارد. این به بازیکن کنترل بیشتری بر عملکرد توپ میدهد، اما نحوه دقیق آن به هیچ وجه به جز از طریق بازی به بازیکن منتقل نمیشود.
حالا که چوب بیسبال دارید که با آن به توپ ضربه بزنید، خیلی خوب میشود اگر چند آجر هم داشته باشید که با توپ بشکنید!
۸. دیوار را فرو بریزید
آجرها را ایجاد کنید
برای اضافه کردن آجر به بازی،
- تعدادی ثابت را به صورت زیر در فایل
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.
- کامپوننت
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;
}
}
اگر بازی را اجرا کنید، تمام مکانیکهای کلیدی بازی نمایش داده میشود. میتوانید اشکالزدایی را غیرفعال کنید و آن را تمامشده بنامید، اما احساس میکنید چیزی کم است.

نظرتان در مورد یک صفحه خوشامدگویی، یک صفحه پایان بازی و شاید یک امتیاز چیست؟ فلاتر میتواند این ویژگیها را به بازی اضافه کند و اینجاست که توجه شما را جلب خواهد کرد.
۹. بازی را ببرید
حالتهای پخش را اضافه کنید
در این مرحله، بازی Flame را درون یک پوشش Flutter جاسازی میکنید و سپس پوششهای Flutter را برای صفحات خوشامدگویی، پایان بازی و برنده شدن اضافه میکنید.
ابتدا، فایلهای بازی و کامپوننت را تغییر میدهید تا حالتی برای اجرا پیادهسازی شود که نشان دهد آیا یک پوشش نمایش داده شود یا خیر، و اگر چنین است، کدام یک.
- بازی
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 تقسیم میکنید. قبل از این تغییر، فقط میتوانستید با راهاندازی مجدد بازی، یک بازی جدید را شروع کنید. با این اضافات جدید، بازیکن اکنون میتواند بدون چنین اقدامات شدیدی، یک بازی جدید را شروع کند.
برای اینکه به بازیکن اجازه دهید یک بازی جدید را شروع کند، دو کنترلکننده جدید برای بازی پیکربندی کردهاید. یک کنترلکننده ضربه اضافه کردهاید و کنترلکننده صفحهکلید را گسترش دادهاید تا کاربر بتواند یک بازی جدید را در چندین حالت شروع کند. با مدلسازی حالت بازی، منطقی است که اجزا را بهروزرسانی کنید تا هنگام برد یا باخت بازیکن، انتقال حالت بازی را فعال کنند.
- کامپوننت
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 را فعال میکند. اگر بازیکن اجازه دهد توپ از پایین صفحه فرار کند، این باید تقریباً درست به نظر برسد.
- کامپوننت
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 را اضافه کنید.
- یک پوشهی
widgetsدر مسیرlib/srcایجاد کنید. - یک فایل
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 که در این نقشه نیست، منجر به چهرههای ناراضی در اطراف میشود.
- برای دریافت این قابلیت جدید روی صفحه، فایل
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، شما دو فایل برای ویرایش دارید.
- فایل
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>
- فایل
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 قرار میدهید. این کار به کد بازی این امکان را میدهد که هر بار که بازیکن آجری را میشکند، امتیاز را بهروزرسانی کند.
- بازی
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) گره میزنید.
- کلاس
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>());
}
}
}
یک بازی زیبا بسازید
حالا که میتوانید امتیاز خود را در فلاتر ثبت کنید، وقت آن رسیده که ویجتها را کنار هم قرار دهید تا ظاهر خوبی داشته باشند.
- Create
score_card.dartinlib/src/widgetsand 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!,
),
);
},
);
}
}
- Create
overlay_screen.dartinlib/src/widgetsand 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.
|
|
۱۱. تبریک
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...
- Building next generation UIs in Flutter
- Take your Flutter app from boring to beautiful
- Adding in-app purchases to your Flutter app

