1. قبل از شروع
Flame یک موتور بازی دو بعدی مبتنی بر فلاتر است. در این کد لبه، شما یک بازی می سازید که از یک شبیه سازی فیزیک دو بعدی در راستای خطوط Box2D به نام Forge2D استفاده می کند. شما از اجزای Flame برای ترسیم واقعیت فیزیکی شبیه سازی شده روی صفحه استفاده می کنید تا کاربران با آن بازی کنند. پس از تکمیل، بازی شما باید شبیه این گیف متحرک باشد:
پیش نیازها
- تکمیل مقدمهای بر Flame با Flutter Codelab
چیزی که یاد می گیرید
- اصول اولیه Forge2D چگونه کار می کند، با انواع مختلف بدن های فیزیکی شروع می شود.
- نحوه راه اندازی یک شبیه سازی فیزیکی به صورت دو بعدی
آنچه شما نیاز دارید
- فلاتر SDK
- کد ویژوال استودیو (VS Code) با پلاگین های Flutter و Dart
نرم افزار کامپایلر برای هدف توسعه انتخابی شما. این کد لبه برای هر شش پلت فرمی که Flutter پشتیبانی می کند کار می کند. برای هدف قرار دادن ویندوز به Visual Studio، Xcode برای هدف قرار دادن macOS یا iOS و Android Studio برای هدف قرار دادن اندروید نیاز دارید.
2. یک پروژه ایجاد کنید
پروژه Flutter خود را ایجاد کنید
راه های زیادی برای ایجاد پروژه فلاتر وجود دارد. در این قسمت از خط فرمان برای اختصار استفاده می کنید.
برای شروع، این مراحل را دنبال کنید:
- در یک خط فرمان، یک پروژه Flutter ایجاد کنید:
$ flutter create --empty forge2d_game Creating project forge2d_game... Resolving dependencies in forge2d_game... (4.7s) Got dependencies in forge2d_game. Wrote 128 files. All done! You can find general documentation for Flutter at: https://docs.flutter.dev/ Detailed API documentation is available at: https://api.flutter.dev/ If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev In order to run your empty application, type: $ cd forge2d_game $ flutter run Your empty application code is in forge2d_game/lib/main.dart.
- وابستگی های پروژه را برای اضافه کردن Flame و Forge2D تغییر دهید:
$ cd forge2d_game $ flutter pub add characters flame flame_forge2d flame_kenney_xml xml Resolving dependencies... Downloading packages... characters 1.3.0 (from transitive dependency to direct dependency) collection 1.18.0 (1.19.0 available) + flame 1.18.0 + flame_forge2d 0.18.1 + flame_kenney_xml 0.1.0 flutter_lints 3.0.2 (4.0.0 available) + forge2d 0.13.0 leak_tracker 10.0.4 (10.0.5 available) leak_tracker_flutter_testing 3.0.3 (3.0.5 available) lints 3.0.0 (4.0.0 available) material_color_utilities 0.8.0 (0.12.0 available) meta 1.12.0 (1.15.0 available) + ordered_set 5.0.3 (6.0.1 available) + petitparser 6.0.2 test_api 0.7.0 (0.7.3 available) vm_service 14.2.1 (14.2.4 available) + xml 6.5.0 Changed 8 dependencies! 10 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
بسته flame
برای شما آشناست، اما سه مورد دیگر ممکن است نیاز به توضیح داشته باشند. بسته characters
برای دستکاری مسیر فایل به روشی مطابق با UTF8 استفاده می شود. بسته flame_forge2d
عملکرد Forge2D را به گونه ای نشان می دهد که به خوبی با Flame کار می کند. در نهایت، بسته xml
در مکان های مختلف برای مصرف و اصلاح محتوای XML استفاده می شود.
پروژه را باز کنید و سپس محتوای فایل lib/main.dart
را با موارد زیر جایگزین کنید:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
GameWidget.controlled(
gameFactory: FlameGame.new,
),
);
}
این برنامه را با یک GameWidget
شروع می کند که نمونه FlameGame
نمونه سازی می کند. هیچ کد Flutter در این Codelab وجود ندارد که از وضعیت نمونه بازی برای نمایش اطلاعات مربوط به بازی در حال اجرا استفاده کند، بنابراین این بوت استرپ ساده شده به خوبی کار می کند.
اختیاری: یک جستجوی جانبی فقط macOS داشته باشید
اسکرین شات های این پروژه از بازی به عنوان یک برنامه دسکتاپ macOS است. برای اینکه نوار عنوان برنامه باعث کاهش تجربه کلی نشود، میتوانید پیکربندی پروژه macOS runner را تغییر دهید تا نوار عنوان حذف شود.
برای انجام این کار، این مراحل را دنبال کنید:
- یک فایل
bin/modify_macos_config.dart
ایجاد کنید و محتوای زیر را اضافه کنید:
bin/modify_macos_config.dart
import 'dart:io';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';
void main() {
final file = File('macos/Runner/Base.lproj/MainMenu.xib');
var document = XmlDocument.parse(file.readAsStringSync());
document.xpath('//document/objects/window').first
..setAttribute('titlebarAppearsTransparent', 'YES')
..setAttribute('titleVisibility', 'hidden');
document
.xpath('//document/objects/window/windowStyleMask')
.first
.setAttribute('fullSizeContentView', 'YES');
file.writeAsStringSync(document.toString());
}
این فایل در دایرکتوری lib
نیست زیرا بخشی از پایگاه کد زمان اجرا برای بازی نیست. این یک ابزار خط فرمان است که برای اصلاح پروژه استفاده می شود.
- از دایرکتوری پایه پروژه، ابزار را به صورت زیر اجرا کنید:
$ dart bin/modify_macos_config.dart
اگر همه چیز طبق برنامه پیش برود، برنامه هیچ خروجی در خط فرمان تولید نخواهد کرد. با این حال، فایل پیکربندی macos/Runner/Base.lproj/MainMenu.xib
را تغییر میدهد تا بازی را بدون نوار عنوان قابل مشاهده اجرا کند و بازی Flame کل پنجره را اشغال کند.
بازی را اجرا کنید تا مطمئن شوید همه چیز کار می کند. باید یک پنجره جدید با یک پس زمینه سیاه خالی نمایش دهد.
3. دارایی های تصویر را اضافه کنید
تصاویر را اضافه کنید
هر بازی به دارایی های هنری نیاز دارد تا بتواند صفحه نمایش را به گونه ای نقاشی کند که از سرگرمی استفاده کند. این کد آزمایشگاه از بسته دارایی های فیزیک از Kenney.nl استفاده می کند. این داراییها دارای مجوز Creative Commons CC0 هستند، اما من همچنان اکیداً پیشنهاد میکنم که به تیم Kenney کمک مالی کنید تا بتوانند به کار بزرگی که انجام میدهند ادامه دهند. من انجام دادم.
شما باید فایل پیکربندی pubspec.yaml
را تغییر دهید تا امکان استفاده از دارایی های Kenney فراهم شود. آن را به صورت زیر اصلاح کنید:
pubspec.yaml
name: forge2d_game
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.3.3 <4.0.0'
dependencies:
characters: ^1.3.0
flame: ^1.17.0
flame_forge2d: ^0.18.0
flutter:
sdk: flutter
xml: ^6.5.0
dev_dependencies:
flutter_lints: ^3.0.0
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets: # Add from here
- assets/
- assets/images/ # To here.
Flame انتظار دارد که داراییهای تصویر در assets/images
قرار گیرند، اگرچه میتوان آن را متفاوت پیکربندی کرد. برای جزئیات بیشتر به مستندات تصاویر Flame مراجعه کنید. اکنون که مسیرها را پیکربندی کرده اید، باید آنها را به خود پروژه اضافه کنید. یکی از راه های انجام این کار استفاده از خط فرمان به صورت زیر است:
$ mkdir -p assets/images
هیچ خروجی از دستور mkdir
نباید وجود داشته باشد، اما دایرکتوری جدید باید در ویرایشگر شما یا کاوشگر فایل قابل مشاهده باشد.
فایل kenney_physics-assets.zip
را که دانلود کرده اید گسترش دهید، و باید چیزی شبیه به این را ببینید:
از پوشه PNG/Backgrounds
، فایلهای colored_desert.png
، colored_grass.png
، colored_land.png
و colored_shroom.png
را در فهرست assets/images
پروژه خود کپی کنید.
ورق های اسپرایت نیز وجود دارد. اینها ترکیبی از یک تصویر PNG و یک فایل XML هستند که توضیح میدهند در کجای تصویر صفحه اسپریت ممکن است تصاویر کوچکتری پیدا شوند. Spritesheets تکنیکی برای کاهش زمان بارگذاری تنها با بارگذاری یک فایل است در مقابل ده ها، اگر نه صدها فایل تصویری مجزا.
از spritesheet_aliens.png
، spritesheet_elements.png
و spritesheet_tiles.png
در فهرست assets/images
پروژه خود کپی کنید. وقتی اینجا هستید، فایلهای spritesheet_aliens.xml
، spritesheet_elements.xml
و spritesheet_tiles.xml
را نیز در فهرست assets
پروژه خود کپی کنید. پروژه شما باید شبیه زیر باشد.
پس زمینه را رنگ کنید
اکنون که پروژه شما دارایی های تصویری اضافه شده است، وقت آن است که آنها را روی صفحه نمایش دهید. خوب، یک تصویر روی صفحه نمایش. در مراحل زیر بیشتر خواهد آمد.
یک فایل به نام background.dart
در دایرکتوری جدیدی به نام lib/components
ایجاد کنید و محتوای زیر را اضافه کنید.
lib/components/background.dart
import 'dart:math';
import 'package:flame/components.dart';
import 'game.dart';
class Background extends SpriteComponent with HasGameReference<MyPhysicsGame> {
Background({required super.sprite})
: super(
anchor: Anchor.center,
position: Vector2(0, 0),
);
@override
void onMount() {
super.onMount();
size = Vector2.all(max(
game.camera.visibleWorldRect.width,
game.camera.visibleWorldRect.height,
));
}
}
این کامپوننت یک SpriteComponent
تخصصی است. این مسئول نمایش یکی از چهار تصویر پس زمینه Kenney.nl است. چند فرض ساده کننده در این کد وجود دارد. اولین مورد این است که تصاویر مربع هستند، که هر چهار تصویر پس زمینه از Kenney هستند. دوم این است که اندازه جهان قابل مشاهده هرگز تغییر نخواهد کرد، در غیر این صورت این جزء باید به رویدادهای تغییر اندازه بازی رسیدگی کند. فرض سوم این است که موقعیت (0,0) در مرکز صفحه باشد. این مفروضات به پیکربندی خاصی از CameraComponent
بازی نیاز دارند.
یک فایل جدید دیگر، به نام game.dart
، دوباره در دایرکتوری lib/components
ایجاد کنید.
lib/components/game.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
return super.onLoad();
}
}
اینجا خیلی اتفاق می افتد. بیایید با کلاس MyPhysicsGame
شروع کنیم. بر خلاف کدهای قبلی، این Forge2DGame
گسترش می دهد نه FlameGame
. خود Forge2DGame
FlameGame
با چند ترفند جالب گسترش می دهد. اولین مورد این است که بهطور پیشفرض، zoom
روی 10 تنظیم شده است. این تنظیم zoom
مربوط به محدوده مقادیر مفیدی است که موتورهای شبیهسازی فیزیک سبک Box2D
به خوبی با آن کار میکنند. موتور با استفاده از سیستم MKS نوشته شده است که در آن واحدها بر حسب متر، کیلوگرم و ثانیه فرض می شوند. محدوده ای که در آن خطاهای ریاضی قابل توجهی برای اشیا مشاهده نمی کنید از 0.1 متر تا 10 ثانیه متر است. تغذیه در ابعاد پیکسل به طور مستقیم بدون مقداری کاهش مقیاس، Forge2D را از پوشش مفید خود خارج می کند. خلاصه مفید این است که به شبیه سازی اجسام در محدوده قوطی نوشابه تا اتوبوس فکر کنید.
فرضیات ساخته شده در مولفه Background در اینجا با تثبیت وضوح CameraComponent
روی 800 در 600 پیکسل مجازی برآورده می شود. این بدان معنی است که منطقه بازی 80 واحد عرض و 60 واحد ارتفاع خواهد داشت که در مرکز (0,0) قرار دارد. این هیچ تاثیری بر وضوح نمایش داده شده ندارد، اما در جایی که ما اشیاء را در صحنه بازی قرار می دهیم، تاثیر می گذارد.
در کنار آرگومان سازنده camera
، آرگومان دیگری هم تراز با فیزیک به نام gravity
وجود دارد. گرانش بر روی یک Vector2
با x
0 و y
از 10 تنظیم شده است. این واقعیت که گرانش روی مثبت 10 تنظیم شده است نشان می دهد که در این سیستم جهت محور Y پایین است. که به طور کلی با Box2D متفاوت است، اما مطابق با نحوه پیکربندی Flame است.
بعدی روش onLoad
است. این روش ناهمزمان است، که مناسب است زیرا مسئول بارگذاری دارایی های تصویر از دیسک است. فراخوانی به images.load
یک Future<Image>
را برمی گرداند و به عنوان یک اثر جانبی تصویر بارگذاری شده را در شی Game ذخیره می کند. این قراردادهای آتی با هم جمعآوری میشوند و با استفاده از روش استاتیک Futures.wait
به عنوان یک واحد در انتظار میروند. سپس لیست تصاویر برگشتی با نام های فردی مطابقت داده می شود.
سپس تصاویر spriteshet به یک سری از اشیاء XmlSpriteSheet
وارد می شوند که مسئول بازیابی Sprites با نام جداگانه موجود در spriteshe هستند. کلاس XmlSpriteSheet
در بسته flame_kenney_xml
تعریف شده است.
با وجود تمام این موارد، شما فقط به چند ویرایش جزئی در lib/main.dart
نیاز دارید تا تصویری را روی صفحه نمایش دهید.
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'components/game.dart'; // Add this import
void main() {
runApp(
GameWidget.controlled(
gameFactory: MyPhysicsGame.new, // Modify this line
),
);
}
با این تغییر ساده اکنون می توانید بازی را دوباره اجرا کنید تا پس زمینه را روی صفحه ببینید. توجه داشته باشید، نمونه دوربین CameraComponent.withFixedResolution()
جعبه نامه را در صورت نیاز اضافه می کند تا نسبت 800 به 600 بازی را به کار گیرد.
4. زمین را اضافه کنید
چیزی برای ساختن
اگر جاذبه داشته باشیم، به چیزی نیاز داریم تا اشیا را در بازی قبل از افتادن از پایین صفحه بگیریم. البته، مگر اینکه افتادن از صفحه نمایش بخشی از طراحی بازی شما باشد. یک فایل ground.dart
جدید در دایرکتوری lib/components
خود ایجاد کنید و موارد زیر را به آن اضافه کنید:
lib/components/ground.dart
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
const groundSize = 7.0;
class Ground extends BodyComponent {
Ground(Vector2 position, Sprite sprite)
: super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.static,
fixtureDefs: [
FixtureDef(
PolygonShape()..setAsBoxXY(groundSize / 2, groundSize / 2),
friction: 0.3,
)
],
children: [
SpriteComponent(
anchor: Anchor.center,
sprite: sprite,
size: Vector2.all(groundSize),
position: Vector2(0, 0),
),
],
);
}
این مولفه Ground
از BodyComponent
مشتق شده است. در Forge2D اجسام مهم هستند، آنها اجسامی هستند که بخشی از شبیه سازی فیزیکی دو بعدی هستند. BodyDef
برای این جزء مشخص شده است که BodyType.static
داشته باشد.
در Forge2D بدن ها سه نوع مختلف دارند. اجسام ساکن حرکت نمی کنند. آنها عملاً هم جرم صفر دارند - به گرانش واکنش نشان نمی دهند - و هم جرم بی نهایت - در هنگام برخورد با اجسام دیگر حرکت نمی کنند، مهم نیست چقدر سنگین هستند. این اجسام ساکن را برای سطح زمین عالی می کند، زیرا حرکت نمی کند.
دو نوع دیگر بدن، سینماتیک و دینامیک هستند. اجسام پویا اجسامی هستند که به طور کامل شبیه سازی شده اند، به گرانش و اجسامی که به آنها برخورد می کنند واکنش نشان می دهند. بدنه های دینامیکی زیادی را در بقیه قسمت های این Codelab خواهید دید. اجسام سینماتیک یک خانه بین ایستا و دینامیک هستند. آنها حرکت می کنند، اما در برابر جاذبه یا اشیاء دیگر که به آنها برخورد می کنند واکنشی نشان نمی دهند. مفید است، اما فراتر از محدوده این نرم افزار.
خود بدن کار زیادی انجام نمی دهد. یک بدن برای داشتن ماده به اشکال مرتبط نیاز دارد. در این مورد، این بدنه یک شکل مرتبط دارد، یک مجموعه PolygonShape
به عنوان یک BoxXY
. این نوع جعبه برخلاف یک مجموعه PolygonShape
به عنوان یک BoxXY
که می تواند حول یک نقطه چرخش بچرخد، با جهان تراز است. باز هم مفید است، اما همچنین خارج از محدوده این Codelab. شکل و بدنه با یک فیکسچر به هم متصل شده اند که برای افزودن مواردی مانند friction
به سیستم مفید است.
بهطور پیشفرض، یک بدنه شکلهای متصل خود را به گونهای ارائه میکند که برای اشکالزدایی مفید است، اما گیمپلی عالی را ایجاد نمیکند. تنظیم super
آرگومان renderBody
روی false
این اشکال زدایی را غیرفعال می کند. دادن رندر درون بازی به این بدنه بر عهده فرزند SpriteComponent
است.
برای افزودن کامپوننت Ground
به بازی، فایل game.dart
خود را به صورت زیر ویرایش کنید.
lib/components/game.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
import 'ground.dart'; // Add this import
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround(); // Add this line
return super.onLoad();
}
Future<void> addGround() { // Add from here...
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
} // To here.
}
این ویرایش با استفاده از یک حلقه for
در داخل یک متن List
، و ارسال لیست حاصل از اجزای Ground
به متد world
addAll
یک سری اجزای Ground
را به جهان اضافه می کند.
اکنون اجرای بازی پس زمینه و زمین را نشان می دهد.
5. آجرها را اضافه کنید
ساختن دیوار
زمین نمونه ای از یک جسم ساکن را به ما داد. اکنون زمان اولین مؤلفه پویا است. اجزای پویا در Forge2D سنگ بنای تجربه بازیکن هستند، آنها چیزهایی هستند که حرکت می کنند و با دنیای اطراف خود تعامل دارند. در این مرحله آجرهایی را معرفی میکنید که بهطور تصادفی انتخاب میشوند تا در دستهای از آجرها روی صفحه ظاهر شوند. در حین انجام این کار، آنها را خواهید دید که می افتند و با یکدیگر برخورد می کنند.
آجرها از ورق اسپرایت عناصر ساخته می شوند. اگر به توضیحات صفحه sprite در assets/spritesheet_elements.xml
نگاه کنید، می بینید که مشکل جالبی داریم. به نظر می رسد نام ها چندان مفید نیستند. آنچه مفید است می تواند یک آجر را بر اساس نوع مواد، اندازه آن و میزان آسیب انتخاب کند. خوشبختانه، یک جن مفید مدتی را صرف کشف الگوی نامگذاری فایل کرد و ابزاری برای آسانتر کردن آن برای همه شما ایجاد کرد. یک فایل جدید generate_brick_file_names.dart
در پوشه bin
ایجاد کنید و محتوای زیر را اضافه کنید:
bin/generate_brick_file_names.dart
import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';
void main() {
final file = File('assets/spritesheet_elements.xml');
final rects = <String, Rect>{};
final document = XmlDocument.parse(file.readAsStringSync());
for (final node in document.xpath('//TextureAtlas/SubTexture')) {
final name = node.getAttribute('name')!;
rects[name] = Rect(
x: int.parse(node.getAttribute('x')!),
y: int.parse(node.getAttribute('y')!),
width: int.parse(node.getAttribute('width')!),
height: int.parse(node.getAttribute('height')!),
);
}
print(generateBrickFileNames(rects));
}
class Rect extends Equatable {
final int x;
final int y;
final int width;
final int height;
const Rect(
{required this.x,
required this.y,
required this.width,
required this.height});
Size get size => Size(width, height);
@override
List<Object?> get props => [x, y, width, height];
@override
bool get stringify => true;
}
class Size extends Equatable {
final int width;
final int height;
const Size(this.width, this.height);
@override
List<Object?> get props => [width, height];
@override
bool get stringify => true;
}
String generateBrickFileNames(Map<String, Rect> rects) {
final groups = <Size, List<String>>{};
for (final entry in rects.entries) {
groups.putIfAbsent(entry.value.size, () => []).add(entry.key);
}
final buff = StringBuffer();
buff.writeln('''
Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
return switch ((type, size)) {''');
for (final entry in groups.entries) {
final size = entry.key;
final entries = entry.value;
entries.sort();
for (final type in ['Explosive', 'Glass', 'Metal', 'Stone', 'Wood']) {
var filtered = entries.where((element) => element.contains(type));
if (filtered.length == 5) {
buff.writeln('''
(BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
BrickDamage.none: '${filtered.elementAt(0)}',
BrickDamage.some: '${filtered.elementAt(1)}',
BrickDamage.lots: '${filtered.elementAt(4)}',
},''');
} else if (filtered.length == 10) {
buff.writeln('''
(BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
BrickDamage.none: '${filtered.elementAt(3)}',
BrickDamage.some: '${filtered.elementAt(4)}',
BrickDamage.lots: '${filtered.elementAt(9)}',
},''');
} else if (filtered.length == 15) {
buff.writeln('''
(BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
BrickDamage.none: '${filtered.elementAt(7)}',
BrickDamage.some: '${filtered.elementAt(8)}',
BrickDamage.lots: '${filtered.elementAt(13)}',
},''');
}
}
}
buff.writeln('''
};
}''');
return buff.toString();
}
ویرایشگر شما باید به شما هشدار یا خطایی در مورد یک وابستگی از دست رفته بدهد. به صورت زیر اضافه کنید:
$ flutter pub add equatable
اکنون باید بتوانید این برنامه را به صورت زیر اجرا کنید:
$ dart run bin/generate_brick_file_names.dart Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) { return switch ((type, size)) { (BrickType.explosive, BrickSize.size140x70) => { BrickDamage.none: 'elementExplosive009.png', BrickDamage.some: 'elementExplosive012.png', BrickDamage.lots: 'elementExplosive050.png', }, (BrickType.glass, BrickSize.size140x70) => { BrickDamage.none: 'elementGlass010.png', BrickDamage.some: 'elementGlass013.png', BrickDamage.lots: 'elementGlass048.png', }, [Content elided...] (BrickType.wood, BrickSize.size140x220) => { BrickDamage.none: 'elementWood020.png', BrickDamage.some: 'elementWood025.png', BrickDamage.lots: 'elementWood052.png', }, }; }
این ابزار به کمک فایل توضیحات صفحه sprite را تجزیه کرده و آن را به کد دارت تبدیل کرده است که میتوانیم از آن برای انتخاب فایل تصویر مناسب برای هر آجری که میخواهید روی صفحه نمایش دهید استفاده کنیم. مفید!
فایل brick.dart
را با محتوای زیر ایجاد کنید:
lib/components/brick.dart
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
const brickScale = 0.5;
enum BrickType {
explosive(density: 1, friction: 0.5),
glass(density: 0.5, friction: 0.2),
metal(density: 1, friction: 0.4),
stone(density: 2, friction: 1),
wood(density: 0.25, friction: 0.6);
final double density;
final double friction;
const BrickType({required this.density, required this.friction});
static BrickType get randomType => values[Random().nextInt(values.length)];
}
enum BrickSize {
size70x70(ui.Size(70, 70)),
size140x70(ui.Size(140, 70)),
size220x70(ui.Size(220, 70)),
size70x140(ui.Size(70, 140)),
size140x140(ui.Size(140, 140)),
size220x140(ui.Size(220, 140)),
size140x220(ui.Size(140, 220)),
size70x220(ui.Size(70, 220));
final ui.Size size;
const BrickSize(this.size);
static BrickSize get randomSize => values[Random().nextInt(values.length)];
}
enum BrickDamage { none, some, lots }
Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
return switch ((type, size)) {
(BrickType.explosive, BrickSize.size140x70) => {
BrickDamage.none: 'elementExplosive009.png',
BrickDamage.some: 'elementExplosive012.png',
BrickDamage.lots: 'elementExplosive050.png',
},
(BrickType.glass, BrickSize.size140x70) => {
BrickDamage.none: 'elementGlass010.png',
BrickDamage.some: 'elementGlass013.png',
BrickDamage.lots: 'elementGlass048.png',
},
(BrickType.metal, BrickSize.size140x70) => {
BrickDamage.none: 'elementMetal009.png',
BrickDamage.some: 'elementMetal012.png',
BrickDamage.lots: 'elementMetal050.png',
},
(BrickType.stone, BrickSize.size140x70) => {
BrickDamage.none: 'elementStone009.png',
BrickDamage.some: 'elementStone012.png',
BrickDamage.lots: 'elementStone047.png',
},
(BrickType.wood, BrickSize.size140x70) => {
BrickDamage.none: 'elementWood011.png',
BrickDamage.some: 'elementWood014.png',
BrickDamage.lots: 'elementWood054.png',
},
(BrickType.explosive, BrickSize.size70x70) => {
BrickDamage.none: 'elementExplosive011.png',
BrickDamage.some: 'elementExplosive014.png',
BrickDamage.lots: 'elementExplosive049.png',
},
(BrickType.glass, BrickSize.size70x70) => {
BrickDamage.none: 'elementGlass011.png',
BrickDamage.some: 'elementGlass012.png',
BrickDamage.lots: 'elementGlass046.png',
},
(BrickType.metal, BrickSize.size70x70) => {
BrickDamage.none: 'elementMetal011.png',
BrickDamage.some: 'elementMetal014.png',
BrickDamage.lots: 'elementMetal049.png',
},
(BrickType.stone, BrickSize.size70x70) => {
BrickDamage.none: 'elementStone011.png',
BrickDamage.some: 'elementStone014.png',
BrickDamage.lots: 'elementStone046.png',
},
(BrickType.wood, BrickSize.size70x70) => {
BrickDamage.none: 'elementWood010.png',
BrickDamage.some: 'elementWood013.png',
BrickDamage.lots: 'elementWood045.png',
},
(BrickType.explosive, BrickSize.size220x70) => {
BrickDamage.none: 'elementExplosive013.png',
BrickDamage.some: 'elementExplosive016.png',
BrickDamage.lots: 'elementExplosive051.png',
},
(BrickType.glass, BrickSize.size220x70) => {
BrickDamage.none: 'elementGlass014.png',
BrickDamage.some: 'elementGlass017.png',
BrickDamage.lots: 'elementGlass049.png',
},
(BrickType.metal, BrickSize.size220x70) => {
BrickDamage.none: 'elementMetal013.png',
BrickDamage.some: 'elementMetal016.png',
BrickDamage.lots: 'elementMetal051.png',
},
(BrickType.stone, BrickSize.size220x70) => {
BrickDamage.none: 'elementStone013.png',
BrickDamage.some: 'elementStone016.png',
BrickDamage.lots: 'elementStone048.png',
},
(BrickType.wood, BrickSize.size220x70) => {
BrickDamage.none: 'elementWood012.png',
BrickDamage.some: 'elementWood015.png',
BrickDamage.lots: 'elementWood047.png',
},
(BrickType.explosive, BrickSize.size70x140) => {
BrickDamage.none: 'elementExplosive017.png',
BrickDamage.some: 'elementExplosive022.png',
BrickDamage.lots: 'elementExplosive052.png',
},
(BrickType.glass, BrickSize.size70x140) => {
BrickDamage.none: 'elementGlass018.png',
BrickDamage.some: 'elementGlass023.png',
BrickDamage.lots: 'elementGlass050.png',
},
(BrickType.metal, BrickSize.size70x140) => {
BrickDamage.none: 'elementMetal017.png',
BrickDamage.some: 'elementMetal022.png',
BrickDamage.lots: 'elementMetal052.png',
},
(BrickType.stone, BrickSize.size70x140) => {
BrickDamage.none: 'elementStone017.png',
BrickDamage.some: 'elementStone022.png',
BrickDamage.lots: 'elementStone049.png',
},
(BrickType.wood, BrickSize.size70x140) => {
BrickDamage.none: 'elementWood016.png',
BrickDamage.some: 'elementWood021.png',
BrickDamage.lots: 'elementWood048.png',
},
(BrickType.explosive, BrickSize.size140x140) => {
BrickDamage.none: 'elementExplosive018.png',
BrickDamage.some: 'elementExplosive023.png',
BrickDamage.lots: 'elementExplosive053.png',
},
(BrickType.glass, BrickSize.size140x140) => {
BrickDamage.none: 'elementGlass019.png',
BrickDamage.some: 'elementGlass024.png',
BrickDamage.lots: 'elementGlass051.png',
},
(BrickType.metal, BrickSize.size140x140) => {
BrickDamage.none: 'elementMetal018.png',
BrickDamage.some: 'elementMetal023.png',
BrickDamage.lots: 'elementMetal053.png',
},
(BrickType.stone, BrickSize.size140x140) => {
BrickDamage.none: 'elementStone018.png',
BrickDamage.some: 'elementStone023.png',
BrickDamage.lots: 'elementStone050.png',
},
(BrickType.wood, BrickSize.size140x140) => {
BrickDamage.none: 'elementWood017.png',
BrickDamage.some: 'elementWood022.png',
BrickDamage.lots: 'elementWood049.png',
},
(BrickType.explosive, BrickSize.size220x140) => {
BrickDamage.none: 'elementExplosive019.png',
BrickDamage.some: 'elementExplosive024.png',
BrickDamage.lots: 'elementExplosive054.png',
},
(BrickType.glass, BrickSize.size220x140) => {
BrickDamage.none: 'elementGlass020.png',
BrickDamage.some: 'elementGlass025.png',
BrickDamage.lots: 'elementGlass052.png',
},
(BrickType.metal, BrickSize.size220x140) => {
BrickDamage.none: 'elementMetal019.png',
BrickDamage.some: 'elementMetal024.png',
BrickDamage.lots: 'elementMetal054.png',
},
(BrickType.stone, BrickSize.size220x140) => {
BrickDamage.none: 'elementStone019.png',
BrickDamage.some: 'elementStone024.png',
BrickDamage.lots: 'elementStone051.png',
},
(BrickType.wood, BrickSize.size220x140) => {
BrickDamage.none: 'elementWood018.png',
BrickDamage.some: 'elementWood023.png',
BrickDamage.lots: 'elementWood050.png',
},
(BrickType.explosive, BrickSize.size70x220) => {
BrickDamage.none: 'elementExplosive020.png',
BrickDamage.some: 'elementExplosive025.png',
BrickDamage.lots: 'elementExplosive055.png',
},
(BrickType.glass, BrickSize.size70x220) => {
BrickDamage.none: 'elementGlass021.png',
BrickDamage.some: 'elementGlass026.png',
BrickDamage.lots: 'elementGlass053.png',
},
(BrickType.metal, BrickSize.size70x220) => {
BrickDamage.none: 'elementMetal020.png',
BrickDamage.some: 'elementMetal025.png',
BrickDamage.lots: 'elementMetal055.png',
},
(BrickType.stone, BrickSize.size70x220) => {
BrickDamage.none: 'elementStone020.png',
BrickDamage.some: 'elementStone025.png',
BrickDamage.lots: 'elementStone052.png',
},
(BrickType.wood, BrickSize.size70x220) => {
BrickDamage.none: 'elementWood019.png',
BrickDamage.some: 'elementWood024.png',
BrickDamage.lots: 'elementWood051.png',
},
(BrickType.explosive, BrickSize.size140x220) => {
BrickDamage.none: 'elementExplosive021.png',
BrickDamage.some: 'elementExplosive026.png',
BrickDamage.lots: 'elementExplosive056.png',
},
(BrickType.glass, BrickSize.size140x220) => {
BrickDamage.none: 'elementGlass022.png',
BrickDamage.some: 'elementGlass027.png',
BrickDamage.lots: 'elementGlass054.png',
},
(BrickType.metal, BrickSize.size140x220) => {
BrickDamage.none: 'elementMetal021.png',
BrickDamage.some: 'elementMetal026.png',
BrickDamage.lots: 'elementMetal056.png',
},
(BrickType.stone, BrickSize.size140x220) => {
BrickDamage.none: 'elementStone021.png',
BrickDamage.some: 'elementStone026.png',
BrickDamage.lots: 'elementStone053.png',
},
(BrickType.wood, BrickSize.size140x220) => {
BrickDamage.none: 'elementWood020.png',
BrickDamage.some: 'elementWood025.png',
BrickDamage.lots: 'elementWood052.png',
},
};
}
class Brick extends BodyComponent {
Brick({
required this.type,
required this.size,
required BrickDamage damage,
required Vector2 position,
required Map<BrickDamage, Sprite> sprites,
}) : _damage = damage,
_sprites = sprites,
super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.dynamic,
fixtureDefs: [
FixtureDef(
PolygonShape()
..setAsBoxXY(
size.size.width / 20 * brickScale,
size.size.height / 20 * brickScale,
),
)
..restitution = 0.4
..density = type.density
..friction = type.friction
]);
late final SpriteComponent _spriteComponent;
final BrickType type;
final BrickSize size;
final Map<BrickDamage, Sprite> _sprites;
BrickDamage _damage;
BrickDamage get damage => _damage;
set damage(BrickDamage value) {
_damage = value;
_spriteComponent.sprite = _sprites[value];
}
@override
Future<void> onLoad() {
_spriteComponent = SpriteComponent(
anchor: Anchor.center,
scale: Vector2.all(1),
sprite: _sprites[_damage],
size: size.size.toVector2() / 10 * brickScale,
position: Vector2(0, 0),
);
add(_spriteComponent);
return super.onLoad();
}
}
اکنون می توانید ببینید که چگونه کد دارت تولید شده در بالا در این پایگاه کد یکپارچه شده است تا انتخاب تصاویر آجری بر اساس مواد، اندازه و شرایط آسان و سریع باشد. با نگاهی به enum
s و خود مؤلفه Brick
، باید دریابید که بیشتر این کد از مولفه Ground
در مرحله قبل نسبتاً آشنا به نظر می رسد. در اینجا حالتی قابل تغییر وجود دارد که اجازه می دهد آجر آسیب ببیند، اگرچه استفاده از آن به عنوان تمرینی برای خواننده باقی می ماند.
زمان نمایش آجرها بر روی صفحه نمایش است. فایل game.dart
را به صورت زیر ویرایش کنید:
lib/components/game.dart
import 'dart:async';
import 'dart:math'; // Add this import
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
import 'brick.dart'; // Add this import
import 'ground.dart';
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround();
unawaited(addBricks()); // Add this line
return super.onLoad();
}
Future<void> addGround() {
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
}
final _random = Random(); // Add from here...
Future<void> addBricks() async {
for (var i = 0; i < 5; i++) {
final type = BrickType.randomType;
final size = BrickSize.randomSize;
await world.add(
Brick(
type: type,
size: size,
damage: BrickDamage.some,
position: Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 5 - 2.5),
0),
sprites: brickFileNames(type, size).map(
(key, filename) => MapEntry(
key,
elements.getSprite(filename),
),
),
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
} // To here.
}
این افزودن کد با کدی که برای اضافه کردن اجزای Ground
استفاده کردید کمی متفاوت است. این بار Brick
به مرور زمان در یک خوشه تصادفی اضافه می شوند. این دو بخش دارد، اولی این است که روشی که Brick
s await
sa Future.delayed
را اضافه می کند، که معادل ناهمزمان یک فراخوانی sleep()
است. با این حال، بخش دومی برای انجام این کار وجود دارد، فراخوانی به addBricks
در روش onLoad
await
نیست. اگر اینطور بود، روش onLoad
تا زمانی که همه آجرها روی صفحه نمایش داده نمی شدند، کامل نمی شد. قرار دادن تماس با addBricks
در یک تماس unawaited
باعث خوشحالی افراد می شود و هدف ما را برای برنامه نویسان آینده آشکار می کند. عدم انتظار برای بازگشت این روش عمدی است.
بازی را اجرا کنید، خواهید دید که آجرها ظاهر می شوند، به یکدیگر برخورد می کنند و روی زمین می ریزند.
6. پخش کننده را اضافه کنید
پرتاب بیگانگان به سمت آجر
تماشای چرخیدن آجرها در چند بار اول سرگرم کننده است، اما من حدس میزنم این بازی سرگرمکنندهتر خواهد بود اگر به بازیکن آواتاری بدهیم که بتواند از آن برای تعامل با جهان استفاده کند. در مورد یک بیگانه که می توانند به سمت آجرها پرت کنند چطور؟
یک فایل player.dart
جدید در پوشه lib/components
ایجاد کنید و موارد زیر را به آن اضافه کنید:
lib/components/player.dart
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
const playerSize = 5.0;
enum PlayerColor {
pink,
blue,
green,
yellow;
static PlayerColor get randomColor =>
PlayerColor.values[Random().nextInt(PlayerColor.values.length)];
String get fileName =>
'alien${toString().split('.').last.capitalize}_round.png';
}
class Player extends BodyComponent with DragCallbacks {
Player(Vector2 position, Sprite sprite)
: _sprite = sprite,
super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.static
..angularDamping = 0.1
..linearDamping = 0.1,
fixtureDefs: [
FixtureDef(CircleShape()..radius = playerSize / 2)
..restitution = 0.4
..density = 0.75
..friction = 0.5
],
);
final Sprite _sprite;
@override
Future<void> onLoad() {
addAll([
CustomPainterComponent(
painter: _DragPainter(this),
anchor: Anchor.center,
size: Vector2(playerSize, playerSize),
position: Vector2(0, 0),
),
SpriteComponent(
anchor: Anchor.center,
sprite: _sprite,
size: Vector2(playerSize, playerSize),
position: Vector2(0, 0),
)
]);
return super.onLoad();
}
@override
void update(double dt) {
super.update(dt);
if (!body.isAwake) {
removeFromParent();
}
if (position.x > camera.visibleWorldRect.right + 10 ||
position.x < camera.visibleWorldRect.left - 10) {
removeFromParent();
}
}
Vector2 _dragStart = Vector2.zero();
Vector2 _dragDelta = Vector2.zero();
Vector2 get dragDelta => _dragDelta;
@override
void onDragStart(DragStartEvent event) {
super.onDragStart(event);
if (body.bodyType == BodyType.static) {
_dragStart = event.localPosition;
}
}
@override
void onDragUpdate(DragUpdateEvent event) {
if (body.bodyType == BodyType.static) {
_dragDelta = event.localEndPosition - _dragStart;
}
}
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
if (body.bodyType == BodyType.static) {
children
.whereType<CustomPainterComponent>()
.firstOrNull
?.removeFromParent();
body.setType(BodyType.dynamic);
body.applyLinearImpulse(_dragDelta * -50);
add(RemoveEffect(
delay: 5.0,
));
}
}
}
extension on String {
String get capitalize =>
characters.first.toUpperCase() + characters.skip(1).toLowerCase().join();
}
class _DragPainter extends CustomPainter {
_DragPainter(this.player);
final Player player;
@override
void paint(Canvas canvas, Size size) {
if (player.dragDelta != Vector2.zero()) {
var center = size.center(Offset.zero);
canvas.drawLine(
center,
center + (player.dragDelta * -1).toOffset(),
Paint()
..color = Colors.orange.withOpacity(0.7)
..strokeWidth = 0.4
..strokeCap = StrokeCap.round);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
این یک پله بالاتر از اجزای Brick
در مرحله قبل است. این کامپوننت Player
دارای دو جزء فرزند است، یک SpriteComponent
که باید آن را بشناسید و یک CustomPainterComponent
که جدید است. مفهوم CustomPainter
از Flutter است و به شما امکان می دهد روی بوم نقاشی کنید. در اینجا برای بازخورد دادن به بازیکن در مورد اینکه بیگانه دور در هنگام پرتاب شدن به کجا پرواز می کند استفاده می شود.
چگونه بازیکن شروع به پرتاب بیگانه می کند؟ با استفاده از حرکت کشیدن، که مؤلفه Player با تماسهای DragCallbacks
تشخیص میدهد. عقابی که در بین شماست متوجه چیز دیگری در اینجا شده است.
در جایی که اجزای Ground
اجسام ساکن بودند، اجزای آجر اجسام پویا بودند. Player در اینجا ترکیبی از هر دو است. بازیکن به صورت ایستا شروع به کار می کند و منتظر می ماند تا بازیکن آن را بکشد، و با رها کردن درگ، خود را از ایستا به پویا تبدیل می کند، متناسب با درگ، ضربه خطی اضافه می کند و به آواتار بیگانه اجازه پرواز می دهد!
همچنین کدی در کامپوننت Player
وجود دارد که در صورت خارج شدن از محدوده، به خواب رفتن یا اتمام زمان، آن را از صفحه حذف می کند. هدف در اینجا این است که به بازیکن اجازه دهیم بیگانه را پرت کند، ببیند چه اتفاقی میافتد، و سپس یک بار دیگر حرکت کند.
مولفه Player
را با ویرایش game.dart
به صورت زیر در بازی ادغام کنید:
lib/components/game.dart
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
import 'brick.dart';
import 'ground.dart';
import 'player.dart'; // Add this import
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround();
unawaited(addBricks());
await addPlayer(); // Add this line
return super.onLoad();
}
Future<void> addGround() {
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
}
final _random = Random();
Future<void> addBricks() async {
for (var i = 0; i < 5; i++) {
final type = BrickType.randomType;
final size = BrickSize.randomSize;
await world.add(
Brick(
type: type,
size: size,
damage: BrickDamage.some,
position: Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 5 - 2.5),
0),
sprites: brickFileNames(type, size).map(
(key, filename) => MapEntry(
key,
elements.getSprite(filename),
),
),
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}
Future<void> addPlayer() async => world.add( // Add from here...
Player(
Vector2(camera.visibleWorldRect.left * 2 / 3, 0),
aliens.getSprite(PlayerColor.randomColor.fileName),
),
);
@override
void update(double dt) {
super.update(dt);
if (isMounted && world.children.whereType<Player>().isEmpty) {
addPlayer();
}
} // To here.
}
اضافه کردن بازیکن به بازی مشابه اجزای قبلی است که یک چروک اضافی دارد. بیگانه بازیکن طوری طراحی شده است که تحت شرایط خاصی خود را از بازی حذف کند، بنابراین یک کنترل کننده به روز رسانی در اینجا وجود دارد که بررسی می کند آیا کامپوننت Player
در بازی وجود ندارد یا خیر، و اگر چنین است، دوباره یکی را اضافه می کند. اجرای بازی به این صورت است.
7. به تاثیر واکنش نشان دهید
اضافه شدن دشمنان
شما اجسام ایستا و پویا را در تعامل با یکدیگر دیده اید. با این حال، برای اینکه واقعاً به جایی برسید، باید در هنگام برخورد همه چیز در کد پاسخ تماس بگیرید. بیایید ببینیم چگونه این کار انجام می شود. شما قصد دارید چند دشمن را برای بازیباز معرفی کنید. این یک مسیر به یک شرط برنده می دهد - همه دشمنان را از بازی حذف کنید!
یک فایل enemy.dart
در دایرکتوری lib/components
ایجاد کنید و موارد زیر را اضافه کنید:
lib/components/enemy.dart
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'body_component_with_user_data.dart';
const enemySize = 5.0;
enum EnemyColor {
pink(color: 'pink', boss: false),
blue(color: 'blue', boss: false),
green(color: 'green', boss: false),
yellow(color: 'yellow', boss: false),
pinkBoss(color: 'pink', boss: true),
blueBoss(color: 'blue', boss: true),
greenBoss(color: 'green', boss: true),
yellowBoss(color: 'yellow', boss: true);
final bool boss;
final String color;
const EnemyColor({required this.color, required this.boss});
static EnemyColor get randomColor =>
EnemyColor.values[Random().nextInt(EnemyColor.values.length)];
String get fileName =>
'alien${color.capitalize}_${boss ? 'suit' : 'square'}.png';
}
class Enemy extends BodyComponentWithUserData with ContactCallbacks {
Enemy(Vector2 position, Sprite sprite)
: super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.dynamic,
fixtureDefs: [
FixtureDef(
PolygonShape()..setAsBoxXY(enemySize / 2, enemySize / 2),
friction: 0.3,
)
],
children: [
SpriteComponent(
anchor: Anchor.center,
sprite: sprite,
size: Vector2.all(enemySize),
position: Vector2(0, 0),
),
],
);
@override
void beginContact(Object other, Contact contact) {
var interceptVelocity =
(contact.bodyA.linearVelocity - contact.bodyB.linearVelocity)
.length
.abs();
if (interceptVelocity > 35) {
removeFromParent();
}
super.beginContact(other, contact);
}
@override
void update(double dt) {
super.update(dt);
if (position.x > camera.visibleWorldRect.right + 10 ||
position.x < camera.visibleWorldRect.left - 10) {
removeFromParent();
}
}
}
extension on String {
String get capitalize =>
characters.first.toUpperCase() + characters.skip(1).toLowerCase().join();
}
از تعاملات قبلی شما با اجزای Player و Brick، بیشتر این فایل باید آشنا باشد. با این حال، به دلیل کلاس پایه ناشناخته جدید، چند زیر خط قرمز در ویرایشگر شما وجود خواهد داشت. اکنون این کلاس را با افزودن فایلی به نام body_component_with_user_data.dart
به lib/components
با محتوای زیر اضافه کنید:
lib/components/body_component_with_user_data.dart
import 'package:flame_forge2d/flame_forge2d.dart';
class BodyComponentWithUserData extends BodyComponent {
BodyComponentWithUserData({
super.key,
super.bodyDef,
super.children,
super.fixtureDefs,
super.paint,
super.priority,
super.renderBody,
});
@override
Body createBody() {
final body = world.createBody(super.bodyDef!)..userData = this;
fixtureDefs?.forEach(body.createFixture);
return body;
}
}
این کلاس پایه، همراه با callback جدید beginContact
در کامپوننت Enemy
، اساس اطلاعرسانی برنامهای در مورد تأثیرات بین بدنها را تشکیل میدهد. در واقع، شما باید هر مؤلفهای را که میخواهید ویرایش کنید تا اعلانهای تأثیر را بین آنها دریافت کنید. بنابراین، پیش بروید و اجزای Brick
، Ground
و Player
را ویرایش کنید تا از این BodyComponentWithUserData
به جای کلاس پایه BodyComponent
که این اجزا در حال حاضر استفاده می کنند، استفاده کنید. برای مثال، در اینجا نحوه ویرایش مولفه Ground
آمده است:
lib/components/ground.dart
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'body_component_with_user_data.dart'; // Add this import
const groundSize = 7.0;
class Ground extends BodyComponentWithUserData { // Edit this line
Ground(Vector2 position, Sprite sprite)
: super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.static,
fixtureDefs: [
FixtureDef(
PolygonShape()..setAsBoxXY(groundSize / 2, groundSize / 2),
friction: 0.3,
)
],
children: [
SpriteComponent(
anchor: Anchor.center,
sprite: sprite,
size: Vector2.all(groundSize),
position: Vector2(0, 0),
),
],
);
}
برای اطلاعات بیشتر در مورد نحوه مدیریت تماس Forge2d، لطفاً به مستندات Forge2D در مورد تماسهای تماسی مخاطبین مراجعه کنید.
برنده شدن در بازی
اکنون که دشمنانی دارید و راهی برای حذف دشمنان از جهان دارید، یک راه ساده برای تبدیل این شبیه سازی به یک بازی وجود دارد. هدف خود را برای حذف همه دشمنان تعیین کنید! زمان ویرایش فایل game.dart
به شرح زیر است:
lib/components/game.dart
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'package:flutter/material.dart'; // Add this import
import 'background.dart';
import 'brick.dart';
import 'enemy.dart'; // Add this import
import 'ground.dart';
import 'player.dart';
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround();
unawaited(addBricks().then((_) => addEnemies())); // Modify this line
await addPlayer();
return super.onLoad();
}
Future<void> addGround() {
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
}
final _random = Random();
Future<void> addBricks() async {
for (var i = 0; i < 5; i++) {
final type = BrickType.randomType;
final size = BrickSize.randomSize;
await world.add(
Brick(
type: type,
size: size,
damage: BrickDamage.some,
position: Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 5 - 2.5),
0),
sprites: brickFileNames(type, size).map(
(key, filename) => MapEntry(
key,
elements.getSprite(filename),
),
),
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}
Future<void> addPlayer() async => world.add(
Player(
Vector2(camera.visibleWorldRect.left * 2 / 3, 0),
aliens.getSprite(PlayerColor.randomColor.fileName),
),
);
@override
void update(double dt) {
super.update(dt);
if (isMounted && // Modify from here...
world.children.whereType<Player>().isEmpty &&
world.children.whereType<Enemy>().isNotEmpty) {
addPlayer();
}
if (isMounted &&
enemiesFullyAdded &&
world.children.whereType<Enemy>().isEmpty &&
world.children.whereType<TextComponent>().isEmpty) {
world.addAll(
[
(position: Vector2(0.5, 0.5), color: Colors.white),
(position: Vector2.zero(), color: Colors.orangeAccent),
].map(
(e) => TextComponent(
text: 'You win!',
anchor: Anchor.center,
position: e.position,
textRenderer: TextPaint(
style: TextStyle(color: e.color, fontSize: 16),
),
),
),
);
}
}
var enemiesFullyAdded = false;
Future<void> addEnemies() async {
await Future<void>.delayed(const Duration(seconds: 2));
for (var i = 0; i < 3; i++) {
await world.add(
Enemy(
Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 7 - 3.5),
(_random.nextDouble() * 3)),
aliens.getSprite(EnemyColor.randomColor.fileName),
),
);
await Future<void>.delayed(const Duration(seconds: 1));
}
enemiesFullyAdded = true; // To here.
}
}
چالش شما، اگر بخواهید آن را بپذیرید، اجرای بازی و رساندن خود به این صفحه است.
8. تبریک می گویم
تبریک می گویم، شما موفق به ساخت بازی با فلاتر و شعله شدید!
شما یک بازی با استفاده از موتور بازی Flame 2D ساختید و آن را در یک بسته بندی فلاتر جاسازی کردید. شما از Flame's Effects برای متحرک سازی و حذف کامپوننت ها استفاده کردید. شما از فونت های Google و بسته های Flutter Animate استفاده کردید تا کل بازی به خوبی طراحی شده باشد.
بعدش چی؟
برخی از این کدها را بررسی کنید...
- ساخت رابط های کاربری نسل بعدی در فلاتر
- برنامه Flutter خود را از خسته کننده به زیبا تبدیل کنید
- افزودن خریدهای درون برنامه ای به برنامه Flutter خود