1. مقدمه
Flame یک موتور بازی دو بعدی مبتنی بر فلاتر است. در این کد لبه، شما یک بازی با الهام از یکی از کلاسیک های بازی های ویدیویی دهه 70، Breakout استیو وزنیاک خواهید ساخت. شما از اجزای Flame's Components برای کشیدن خفاش، توپ و آجر استفاده خواهید کرد. شما از افکت های Flame برای متحرک سازی حرکت خفاش استفاده خواهید کرد و نحوه ادغام Flame با سیستم مدیریت ایالت فلاتر را مشاهده خواهید کرد.
پس از اتمام، بازی شما باید مانند این گیف متحرک به نظر برسد، البته کمی کندتر.
چیزی که یاد خواهید گرفت
- اصول اولیه Flame چگونه کار می کند، با
GameWidget
شروع می شود. - نحوه استفاده از حلقه بازی
- چگونه
Component
Flame کار می کند آنها شبیه به Flutter'sWidget
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++. همه آنها با فلاتر کار می کنند.
یک هدف توسعه انتخاب کنید
Flutter برنامه هایی را برای چندین پلتفرم تولید می کند. برنامه شما می تواند روی هر یک از سیستم عامل های زیر اجرا شود:
- iOS
- اندروید
- ویندوز
- macOS
- لینوکس
- وب
این یک روش معمول است که یک سیستم عامل را به عنوان هدف توسعه خود انتخاب کنید. این سیستم عاملی است که برنامه شما در طول توسعه روی آن اجرا می شود.
به عنوان مثال: فرض کنید از یک لپ تاپ ویندوزی برای توسعه برنامه Flutter خود استفاده می کنید. سپس اندروید را به عنوان هدف توسعه خود انتخاب می کنید. برای پیش نمایش برنامه خود، یک دستگاه Android را با کابل USB به لپ تاپ ویندوز خود وصل می کنید و برنامه در حال توسعه شما بر روی آن دستگاه اندروید متصل یا در شبیه ساز اندروید اجرا می شود. شما می توانستید ویندوز را به عنوان هدف توسعه انتخاب کنید، که برنامه در حال توسعه شما را به عنوان یک برنامه ویندوز در کنار ویرایشگر شما اجرا می کند.
ممکن است وسوسه شوید که وب را به عنوان هدف توسعه خود انتخاب کنید. این یک جنبه منفی در طول توسعه دارد: شما قابلیت بارگذاری مجدد داغ Flutter Stateful را از دست می دهید. Flutter در حال حاضر نمیتواند برنامههای وب را دوباره بارگیری کند.
قبل از ادامه انتخاب خود را انجام دهید. همیشه می توانید بعداً برنامه خود را روی سایر سیستم عامل ها اجرا کنید. انتخاب یک هدف توسعه، گام بعدی را هموارتر می کند.
فلاتر را نصب کنید
بهروزترین دستورالعملهای نصب Flutter SDK را میتوانید در docs.flutter.dev پیدا کنید.
دستورالعملهای وبسایت Flutter نصب SDK و ابزارهای مرتبط با هدف توسعه و افزونههای ویرایشگر را پوشش میدهد. برای این کد لبه نرم افزار زیر را نصب کنید:
- فلوتر SDK
- کد ویژوال استودیو با افزونه Flutter
- نرم افزار کامپایلر برای هدف توسعه انتخابی شما. (برای هدف قرار دادن ویندوز یا Xcode برای هدف قرار دادن macOS یا iOS به ویژوال استودیو نیاز دارید)
در بخش بعدی، اولین پروژه فلاتر خود را ایجاد خواهید کرد.
اگر نیاز به عیبیابی هر مشکلی دارید، ممکن است برخی از این سؤالات و پاسخها (از StackOverflow) برای عیبیابی مفید باشد.
سوالات متداول
- چگونه مسیر Flutter SDK را پیدا کنم؟
- وقتی دستور Flutter پیدا نشد چه کار کنم؟
- چگونه می توانم مشکل "در انتظار یک فرمان فلاتر دیگر برای آزاد کردن قفل راه اندازی" را برطرف کنم؟
- چگونه به Flutter بگویم که نصب Android SDK من کجاست؟
- چگونه با خطای جاوا هنگام اجرای
flutter doctor --android-licenses
مقابله کنم؟ - چگونه با ابزار Android
sdkmanager
پیدا نشد برخورد کنم؟ - چگونه با خطای "
cmdline-tools
component is missing" برخورد کنم؟ - چگونه CocoaPods را روی Apple Silicon (M1) اجرا کنم؟
- چگونه می توانم قالب بندی خودکار را در ذخیره در VS Code غیرفعال کنم؟
3. یک پروژه ایجاد کنید
اولین پروژه فلاتر خود را ایجاد کنید
این شامل باز کردن VS Code و ایجاد الگوی برنامه Flutter در فهرستی است که انتخاب میکنید.
- کد ویژوال استودیو را اجرا کنید.
- پالت فرمان (
F1
یاCtrl+Shift+P
یاShift+Cmd+P
) را باز کنید و «flutter new» را تایپ کنید. وقتی ظاهر شد، دستور Flutter: New Project را انتخاب کنید.
- برنامه خالی را انتخاب کنید. دایرکتوری را انتخاب کنید که در آن پروژه خود را ایجاد کنید. این باید هر دایرکتوری باشد که به امتیازات بالا نیاز نداشته باشد یا فضایی در مسیر خود نداشته باشد. به عنوان مثال می توان به فهرست اصلی یا
C:\src\
اشاره کرد.
- نام پروژه خود را
brick_breaker
بگذارید. بقیه این کد لبه فرض می کند که نام برنامه خود راbrick_breaker
گذاشته اید.
Flutter اکنون پوشه پروژه شما را ایجاد می کند و VS Code آن را باز می کند. اکنون محتویات دو فایل را با یک داربست اولیه برنامه بازنویسی می کنید.
برنامه اولیه را کپی و جایگذاری کنید
این کد نمونه ارائه شده در این کد لبه را به برنامه شما اضافه می کند.
- در قسمت سمت چپ 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.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
اطلاعات اولیه برنامه شما را مشخص میکند، مانند نسخه فعلی، وابستگیهای آن، و داراییهایی که با آن ارسال میشود.
- فایل
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));
}
- این کد را اجرا کنید تا مطمئن شوید همه چیز کار می کند. باید یک پنجره جدید با یک پس زمینه سیاه خالی نمایش دهد. بدترین بازی ویدیویی جهان اکنون با سرعت 60 فریم بر ثانیه رندر می شود!
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
نیاز دارید.
- فایلی به نام
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);
}
}
جایی که Flutter دارای Widget
s است، Flame دارای Component
است. در جایی که برنامههای Flutter از ایجاد درختهای ویجتها تشکیل شدهاند، بازیهای Flame شامل نگهداری درختهای اجزا هستند.
تفاوت جالبی بین فلاتر و شعله وجود دارد. درخت ویجت Flutter یک توصیف زودگذر است که برای بهروزرسانی لایه RenderObject
پایدار و قابل تغییر ساخته شده است. اجزای Flame پایدار و قابل تغییر هستند، با این انتظار که توسعه دهنده از این اجزا به عنوان بخشی از یک سیستم شبیه سازی استفاده کند.
اجزای Flame برای بیان مکانیک بازی بهینه شده اند. این کد لبه با حلقه بازی شروع میشود که در مرحله بعد مشخص شده است.
- برای کنترل شلوغی، یک فایل حاوی تمام اجزای این پروژه اضافه کنید. یک فایل
components.dart
درlib/src/components
ایجاد کنید و محتوای زیر را اضافه کنید.
lib/src/components/components.dart
export 'play_area.dart';
بخشنامه export
نقش معکوس import
را ایفا می کند. اعلام می کند که این فایل هنگام وارد شدن به فایل دیگری چه عملکردی را نشان می دهد. با افزودن اجزای جدید در مراحل زیر، این فایل ورودی های بیشتری را افزایش می دهد.
یک بازی Flame ایجاد کنید
برای خاموش کردن squiggles قرمز از مرحله قبل، یک زیر کلاس جدید برای Flame's FlameGame
استخراج کنید.
- یک فایل با نام
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
، کد شما دو عمل انجام می دهد.
- بالا سمت چپ را به عنوان لنگر برای منظره یاب پیکربندی می کند. به طور پیش فرض، منظره یاب از وسط منطقه به عنوان لنگر برای
(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));
}
پس از ایجاد این تغییرات، بازی را مجدداً راه اندازی کنید. بازی باید شبیه شکل زیر باشد.
در مرحله بعدی یک توپ به دنیا اضافه می کنید و آن را به حرکت در می آورید!
5. توپ را نمایش دهید
مولفه توپ را ایجاد کنید
قرار دادن یک توپ متحرک روی صفحه شامل ایجاد یک جزء دیگر و افزودن آن به دنیای بازی است.
- محتویات فایل
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
یا تغییر موقعیت در طول زمان را معرفی می کند. 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
اصلی تنظیم شده است، اما به فاصله 1 کاهش می یابد. این کار سرعت توپ را بدون توجه به جهتی که توپ می رود ثابت نگه می دارد. سپس سرعت توپ به اندازه 1/4 ارتفاع بازی افزایش می یابد.
درست کردن این مقادیر مختلف مستلزم تکرارهایی است که به عنوان تست بازی در صنعت نیز شناخته می شود.
آخرین خط صفحه نمایش اشکال زدایی را روشن می کند، که اطلاعات بیشتری را برای کمک به رفع اشکال به صفحه نمایش اضافه می کند.
وقتی بازی را اجرا می کنید، باید شبیه نمایشگر زیر باشد.
هر دو مولفه 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. ضربه زدن به توپ
خفاش را ایجاد کنید
برای اضافه کردن یک چوب برای حفظ توپ در بازی،
- چند ثابت را در فایل
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
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
به این کامپوننت اضافه می کند.
- برای در دسترس قرار دادن
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. دیوار را خراب کنید
ایجاد آجر
برای افزودن آجر به بازی،
- چند ثابت را در فایل
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;
}
}
اگر بازی را به شکل فعلی اجرا کنید، تمام مکانیزم های کلیدی بازی را نمایش می دهد. میتوانید اشکالزدایی را خاموش کنید و آن را تمام شده بنامید، اما چیزی گم شده است.
در مورد یک صفحه خوش آمدگویی، یک بازی روی صفحه، و شاید یک امتیاز چطور؟ فلاتر می تواند این ویژگی ها را به بازی اضافه کند و اینجاست که در مرحله بعدی توجه خود را معطوف خواهید کرد.
9. بازی را برنده شوید
اضافه کردن حالت های بازی
در این مرحله، بازی 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 } // 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
جدید تقسیم می کنید. قبل از این تغییر، فقط از طریق راه اندازی مجدد بازی می توانستید یک بازی جدید را شروع کنید. با این افزوده های جدید، بازیکن اکنون می تواند یک بازی جدید را بدون چنین اقدامات شدید شروع کند.
برای اینکه به بازیکن اجازه دهید یک بازی جدید را شروع کند، دو کنترل کننده جدید را برای بازی پیکربندی کرده اید. یک کنترل کننده ضربه اضافه کردید و کنترل کننده صفحه کلید را گسترش دادید تا کاربر بتواند یک بازی جدید را در چند حالت شروع کند. با مدلسازی حالت بازی، منطقی است که اجزاء را بهروزرسانی کنیم تا زمانی که بازیکن برنده یا میبازد، انتقال حالت بازی آغاز شود.
- کامپوننت
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
Play را ایجاد می کند. در صورتی که بازیکن اجازه دهد توپ از پایین صفحه خارج شود، باید احساس خوبی داشته باشد.
- کامپوننت
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>());
}
}
}
از طرف دیگر، اگر بازیکن بتواند تمام آجرها را بشکند، صفحه نمایش "برنده بازی" را به دست آورده است. آفرین بازیکن، آفرین!
فلاتر را اضافه کنید
برای ارائه جایی برای جاسازی بازی و افزودن همپوشانی حالت بازی، پوسته فلاتر را اضافه کنید.
- یک دایرکتوری
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(
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
اضافه یا حذف کردهاند، هماهنگ باشند. تلاش برای تنظیم پوششی که در این نقشه وجود ندارد منجر به چهرههای ناراضی در اطراف میشود.
- برای دریافت این قابلیت جدید روی صفحه، فایل
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، دو فایل برای ویرایش دارید.
- فایل
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>
اجرای این حالت همانطور که هست باید یک صفحه خوش آمدگویی و یک صفحه بازی برنده یا بازی را در همه سیستم عامل ها نمایش دهد. این صفحهها ممکن است کمی سادهتر باشند و داشتن امتیاز خوب است. بنابراین، حدس بزنید که در مرحله بعدی چه کاری انجام خواهید داد!
10. امتیاز را حفظ کنید
امتیاز را به بازی اضافه کنید
در این مرحله امتیاز بازی را در معرض زمینه فلاتر اطراف قرار می دهید. در این مرحله شما حالت را از بازی 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
به بازی، وضعیت بازی را به مدیریت حالت فلاتر گره میزنید.
- کلاس
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>());
}
}
}
یک بازی زیبا بسازید
اکنون که میتوانید امتیاز را در فلاتر حفظ کنید، وقت آن است که ویجتها را کنار هم قرار دهید تا زیبا به نظر برسد.
-
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!,
),
);
},
);
}
}
-
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 اجرا کنید. بازی باید شبیه به موارد زیر باشد.
11. تبریک می گویم
تبریک می گویم ، شما موفق به ساختن یک بازی با شعله ور و شعله شد!
شما یک بازی را با استفاده از موتور بازی 2D Flame 2D ساخته و آن را در یک بسته بندی فلوتر تعبیه کرده اید. شما از جلوه های شعله برای تحریک و حذف مؤلفه ها استفاده کردید. شما از فونت های Google و بسته های متحرک Flutter استفاده کرده اید تا کل بازی به خوبی طراحی شده باشد.
بعدش چی؟
برخی از این codelabs را بررسی کنید ...
- ایجاد UI های نسل بعدی در Flutter
- برنامه Flutter خود را از خسته کننده گرفته تا زیبا بگیرید
- اضافه کردن خریدهای درون برنامه ای به برنامه Flutter