1. আপনি শুরু করার আগে
শিখা হল একটি ফ্লটার-ভিত্তিক 2D গেম ইঞ্জিন। এই কোডল্যাবে, আপনি একটি গেম তৈরি করেন যা Forge2D নামক Box2D এর লাইন বরাবর একটি 2D পদার্থবিদ্যা সিমুলেশন ব্যবহার করে। আপনি আপনার ব্যবহারকারীদের সাথে খেলার জন্য স্ক্রিনে সিমুলেটেড শারীরিক বাস্তবতা আঁকার জন্য ফ্লেমের উপাদানগুলি ব্যবহার করেন। সম্পূর্ণ হলে, আপনার গেমটি এই অ্যানিমেটেড জিআইএফের মতো হওয়া উচিত:
পূর্বশর্ত
আপনি কি শিখুন
- Forge2D এর মূল বিষয়গুলি কীভাবে কাজ করে, বিভিন্ন ধরণের শারীরিক সংস্থা থেকে শুরু করে।
- কিভাবে 2D এ একটি শারীরিক সিমুলেশন সেট আপ করবেন।
আপনার যা প্রয়োজন
- ফ্লাটার SDK
- ফ্লাটার এবং ডার্ট প্লাগইন সহ ভিজ্যুয়াল স্টুডিও কোড (ভিএস কোড)
আপনার নির্বাচিত উন্নয়ন লক্ষ্যের জন্য কম্পাইলার সফ্টওয়্যার। এই কোডল্যাবটি সমস্ত ছয়টি প্ল্যাটফর্মের জন্য কাজ করে যা Flutter সমর্থন করে। উইন্ডোজকে টার্গেট করতে আপনার ভিজ্যুয়াল স্টুডিও, ম্যাকওএস বা আইওএসকে টার্গেট করার জন্য এক্সকোড এবং অ্যান্ড্রয়েডকে টার্গেট করতে অ্যান্ড্রয়েড স্টুডিওর প্রয়োজন।
2. একটি প্রকল্প তৈরি করুন
আপনার ফ্লাটার প্রকল্প তৈরি করুন
একটি 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.
- শিখা এবং 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 কার্যকারিতাকে এমনভাবে প্রকাশ করে যা শিখার সাথে ভাল কাজ করে। অবশেষে, 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
ইনস্ট্যান্সকে ইনস্ট্যান্টিয়েট করে। এই কোডল্যাবে কোন ফ্লাটার কোড নেই যা চলমান গেম সম্পর্কে তথ্য প্রদর্শন করতে গেমের অবস্থা ব্যবহার করে, তাই এই সরলীকৃত বুটস্ট্র্যাপটি চমৎকারভাবে কাজ করে।
ঐচ্ছিক: একটি macOS শুধুমাত্র সাইড কোয়েস্ট নিন
এই প্রকল্পের স্ক্রিনশটগুলি একটি macOS ডেস্কটপ অ্যাপ হিসাবে গেম থেকে নেওয়া হয়েছে৷ অ্যাপের টাইটেল বারটি সামগ্রিক অভিজ্ঞতা থেকে বিঘ্নিত হওয়া এড়াতে, আপনি শিরোনাম বারটি এলিড করতে macOS রানারের প্রকল্প কনফিগারেশন পরিবর্তন করতে পারেন।
এটি করতে, এই পদক্ষেপগুলি অনুসরণ করুন:
- একটি
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
কনফিগারেশন ফাইলটিকে একটি দৃশ্যমান শিরোনাম বার ছাড়াই এবং ফ্লেম গেমটি পুরো উইন্ডোটি নিয়ে খেলা চালানোর জন্য পরিবর্তন করবে।
সবকিছু কাজ করছে যাচাই করতে গেমটি চালান। এটি শুধুমাত্র একটি ফাঁকা কালো পটভূমি সহ একটি নতুন উইন্ডো প্রদর্শন করা উচিত।
3. ইমেজ সম্পদ যোগ করুন
ছবি যোগ করুন
যেকোন গেমের জন্য আর্ট সম্পদের প্রয়োজন হয় যাতে একটি স্ক্রীন এমনভাবে আঁকা যায় যা মজা খুঁজে পায়। এই কোডল্যাবটি Kenney.nl থেকে ফিজিক্স অ্যাসেট প্যাক ব্যবহার করবে। এই সম্পদগুলি ক্রিয়েটিভ কমন্স CC0 লাইসেন্সপ্রাপ্ত, কিন্তু আমি এখনও দৃঢ়ভাবে পরামর্শ দিচ্ছি কেনির দলকে একটি অনুদান দেওয়ার জন্য যাতে তারা তাদের করা দুর্দান্ত কাজটি চালিয়ে যেতে পারে। আমি করেছি।
কেনির সম্পদের ব্যবহার সক্ষম করতে আপনাকে pubspec.yaml
কনফিগারেশন ফাইলটি পরিবর্তন করতে হবে। এটি নিম্নরূপ সংশোধন করুন:
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.
ফ্লেম আশা করে যে ইমেজ সম্পদগুলি assets/images
অবস্থিত হবে, যদিও এটি ভিন্নভাবে কনফিগার করা যেতে পারে। আরো বিস্তারিত জানার জন্য শিখার ইমেজ ডকুমেন্টেশন দেখুন. এখন আপনার পাথগুলি কনফিগার করা আছে, আপনাকে সেগুলিকে প্রকল্পে যুক্ত করতে হবে। এটি করার একটি উপায় নিম্নরূপ কমান্ড লাইন ব্যবহার করা হয়:
$ 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
ডিরেক্টরিতে অনুলিপি করুন।
এছাড়াও স্প্রাইট শীট আছে। এগুলি হল একটি পিএনজি ইমেজ এবং একটি এক্সএমএল ফাইলের সংমিশ্রণ যা বর্ণনা করে যে স্প্রিটশিট ইমেজে কোথায় ছোট ছবি পাওয়া যাবে। স্প্রাইটশিটগুলি হল একটি কৌশল যা লোডিং সময় কমানোর জন্য শুধুমাত্র একটি ফাইল লোড করার কৌশল যা কয়েকশ নয়, স্বতন্ত্র ইমেজ ফাইলের বিপরীতে।
spritesheet_aliens.png
, spritesheet_elements.png
, এবং spritesheet_tiles.png
জুড়ে কপি করুন আপনার প্রকল্পের assets/images
ডিরেক্টরিতে। আপনি এখানে থাকাকালীন, spritesheet_aliens.xml
, spritesheet_elements.xml
এবং spritesheet_tiles.xml
ফাইলগুলি আপনার প্রকল্পের assets
ডিরেক্টরিতে অনুলিপি করুন৷ আপনার প্রকল্প নিম্নলিখিত মত দেখতে হবে.
পটভূমি আঁকা
এখন যেহেতু আপনার প্রজেক্টে ইমেজ সম্পদ যোগ করা হয়েছে, সেগুলিকে স্ক্রিনে রাখার সময়। ওয়েল, পর্দায় একটি ছবি. নিম্নলিখিত ধাপে আরো আসবে.
lib/components
নামে একটি নতুন ডিরেক্টরিতে background.dart
নামে একটি ফাইল তৈরি করুন এবং নিম্নলিখিত বিষয়বস্তু যোগ করুন।
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 এর চারটি পটভূমি চিত্রগুলির একটি প্রদর্শনের জন্য দায়ী৷ এই কোডে কয়েকটি সরলীকরণ অনুমান রয়েছে। প্রথমটি হল যে ছবিগুলি বর্গাকার, যা কেনির চারটি পটভূমির ছবি। দ্বিতীয়টি হল দৃশ্যমান বিশ্বের আকার কখনই পরিবর্তিত হবে না, অন্যথায় এই উপাদানটিকে গেমের আকার পরিবর্তনের ঘটনাগুলি পরিচালনা করতে হবে। তৃতীয় অনুমান হল যে অবস্থান (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
শৈলীর পদার্থবিদ্যা সিমুলেশন ইঞ্জিনগুলি ভালভাবে কাজ করে। ইঞ্জিনটি এমকেএস সিস্টেম ব্যবহার করে লেখা হয় যেখানে ইউনিটগুলি মিটার, কিলোগ্রাম এবং সেকেন্ডে অনুমান করা হয়। যে পরিসরে আপনি বস্তুর জন্য লক্ষণীয় গাণিতিক ত্রুটিগুলি দেখতে পাচ্ছেন না তা হল 0.1 মিটার থেকে 10 সেকেন্ড মিটার। পিক্সেল ডাইমেনশনে সরাসরি কিছু মাত্রার ডাউন স্কেলিং ছাড়াই খাওয়ালে Forge2D এর দরকারী খামের বাইরে নিয়ে যাবে। দরকারী সারাংশ হল একটি বাস পর্যন্ত সোডা ক্যানের পরিসরে বস্তুর অনুকরণের কথা ভাবা।
পটভূমি উপাদানে তৈরি অনুমান এখানে CameraComponent
রেজোলিউশন 800 বাই 600 ভার্চুয়াল পিক্সেলে ঠিক করে সন্তুষ্ট করা হয়েছে। এর মানে হল যে গেম এরিয়া হবে 80 ইউনিট চওড়া, এবং 60 ইউনিট লম্বা, কেন্দ্রে থাকবে (0,0)। এটি প্রদর্শিত রেজোলিউশনের উপর কোন প্রভাব ফেলে না, তবে গেমের দৃশ্যে আমরা কোথায় বস্তু রাখি তা প্রভাবিত করবে।
camera
কনস্ট্রাক্টর আর্গুমেন্টের পাশাপাশি আরেকটি, আরো পদার্থবিজ্ঞানের সারিবদ্ধ আর্গুমেন্ট যাকে gravity
বলা হয়। মাধ্যাকর্ষণ একটি Vector2
এ সেট করা হয়েছে যার একটি x
0 এবং a y
10 রয়েছে। 10 হল মাধ্যাকর্ষণটির জন্য প্রতি সেকেন্ডে প্রতি সেকেন্ডের মানের 9.81 মিটারের কাছাকাছি। সত্য যে মাধ্যাকর্ষণ ধনাত্মক 10 এ সেট করা হয়েছে তা দেখায় যে এই সিস্টেমে Y অক্ষের দিকটি নিচের দিকে রয়েছে। যেটি সাধারণত Box2D থেকে ভিন্ন, কিন্তু কিভাবে ফ্লেম সাধারণত কনফিগার করা হয় তার সাথে সঙ্গতিপূর্ণ।
পরবর্তীতে onLoad
পদ্ধতি। এই পদ্ধতিটি অ্যাসিঙ্ক্রোনাস, যা উপযুক্ত কারণ এটি ডিস্ক থেকে ইমেজ সম্পদ লোড করার জন্য দায়ী। images.load
এ কলগুলি একটি Future<Image>
ফেরত দেয়, এবং একটি পার্শ্ব প্রতিক্রিয়া হিসাবে গেম অবজেক্টে লোড করা ছবি ক্যাশে করে। এই ফিউচারগুলিকে একত্রিত করা হয় এবং Futures.wait
স্ট্যাটিক পদ্ধতি ব্যবহার করে একটি একক হিসাবে অপেক্ষা করা হয়। প্রত্যাবর্তিত চিত্রগুলির তালিকা তারপর পৃথক নামের সাথে প্যাটার্ন মিলেছে৷
স্প্রাইটশীট চিত্রগুলিকে তারপর XmlSpriteSheet
অবজেক্টের একটি সিরিজে খাওয়ানো হয় যা স্প্রাইটশিটে থাকা স্বতন্ত্রভাবে নামযুক্ত স্প্রাইটগুলি পুনরুদ্ধার করার জন্য দায়ী। 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 by 600 অনুপাতকে গেমের কাজ করার জন্য প্রয়োজনীয় লেটারবক্সিং যোগ করবে।
4. স্থল যোগ করুন
উপর নির্মাণ কিছু
আমাদের যদি মাধ্যাকর্ষণ থাকে, তাহলে আমাদের খেলায় বস্তুগুলিকে পর্দার নীচে পড়ে যাওয়ার আগে ধরার জন্য কিছু দরকার। যদি না পর্দা থেকে পড়ে যাওয়া অবশ্যই আপনার গেম ডিজাইনের অংশ। আপনার lib/components
ডিরেক্টরিতে একটি নতুন ground.dart
ফাইল তৈরি করুন এবং এতে নিম্নলিখিতগুলি যুক্ত করুন:
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 তে, দেহের তিনটি ভিন্ন প্রকার রয়েছে। স্থির দেহ নড়াচড়া করে না। তাদের কার্যকরীভাবে শূন্য ভর উভয়ই রয়েছে - তারা অভিকর্ষের প্রতি প্রতিক্রিয়া দেখায় না - এবং অসীম ভর - তারা যতই ভারী হোক না কেন অন্য বস্তু দ্বারা আঘাত করলে তারা নড়াচড়া করে না। এটি স্থির দেহগুলিকে স্থল পৃষ্ঠের জন্য নিখুঁত করে তোলে, কারণ এটি নড়াচড়া করে না।
অন্য দুটি ধরণের দেহগুলি গতিশীল এবং গতিশীল। গতিশীল দেহগুলি এমন দেহ যা সম্পূর্ণরূপে সিমুলেটেড, তারা মাধ্যাকর্ষণ এবং যে বস্তুগুলিতে তারা আচমকা হয় তার প্রতি প্রতিক্রিয়া জানায়। এই কোডল্যাবের বাকি অংশে আপনি অনেক ডাইনামিক বডি দেখতে পাবেন। কাইনেমেটিক বডি হল স্ট্যাটিক এবং ডাইনামিক এর মধ্যে একটি অর্ধেক ঘর। তারা নড়াচড়া করে, কিন্তু তারা মাধ্যাকর্ষণ বা তাদের আঘাতকারী অন্যান্য বস্তুর প্রতি প্রতিক্রিয়া দেখায় না। দরকারী, কিন্তু এই কোডল্যাবের সুযোগের বাইরে।
শরীর নিজেই অনেক কিছু করে না। একটি শরীরের পদার্থ আছে সংশ্লিষ্ট আকার প্রয়োজন. এই ক্ষেত্রে, এই শরীরের একটি সম্পর্কিত আকৃতি আছে, একটি PolygonShape
একটি BoxXY
হিসাবে সেট করা হয়েছে। এই ধরনের বাক্স বিশ্বের সাথে অক্ষের সাথে সারিবদ্ধ, একটি PolygonShape
সেট একটি BoxXY
এর বিপরীতে যা ঘূর্ণনের একটি বিন্দুতে ঘোরানো যায়। আবার দরকারী, কিন্তু এই কোডল্যাবের সুযোগের বাইরেও। আকৃতি এবং শরীর একটি ফিক্সচারের সাথে একসাথে সংযুক্ত থাকে, যা সিস্টেমে 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.
}
এই সম্পাদনাটি একটি List
প্রসঙ্গের ভিতরে একটি for
ব্যবহার করে এবং world
addAll
পদ্ধতিতে Ground
উপাদানগুলির ফলাফলের তালিকা পাস করার মাধ্যমে বিশ্বে Ground
উপাদানগুলির একটি সিরিজ যুক্ত করে।
গেমটি চালানো এখন ব্যাকগ্রাউন্ড এবং গ্রাউন্ড দেখায়।
5. ইট যোগ করুন
একটি প্রাচীর নির্মাণ
মাটি আমাদের একটি স্থির শরীরের একটি উদাহরণ দিয়েছে. এখন আপনার প্রথম গতিশীল উপাদানের সময়। Forge2D-এর গতিশীল উপাদানগুলি হল খেলোয়াড়ের অভিজ্ঞতার ভিত্তি, এগুলি এমন জিনিস যা তাদের চারপাশের বিশ্বের সাথে নড়াচড়া করে এবং যোগাযোগ করে৷ এই ধাপে, আপনি ইটগুলি প্রবর্তন করবেন, যা এলোমেলোভাবে ইটগুলির একটি ক্লাস্টারে পর্দায় উপস্থিত হওয়ার জন্য বেছে নেওয়া হবে। আপনি তাদের পড়ে দেখতে পাবেন এবং একে অপরের সাথে ধাক্কা খাচ্ছে।
উপাদান স্প্রাইট শীট থেকে ইট তৈরি করা হবে। আপনি যদি assets/spritesheet_elements.xml
এ স্প্রাইট শীটের বিবরণ দেখেন তবে আপনি দেখতে পাবেন আমাদের একটি আকর্ষণীয় সমস্যা রয়েছে। নামগুলো খুব একটা সহায়ক বলে মনে হচ্ছে না। উপাদানের ধরন, এর আকার এবং ক্ষতির পরিমাণ দ্বারা একটি ইট নির্বাচন করতে কি দরকারী হবে। সৌভাগ্যক্রমে, একটি সহায়ক এলফ ফাইল নামকরণের প্যাটার্নটি বের করতে কিছু সময় ব্যয় করেছে এবং এটিকে সবার জন্য সহজ করার জন্য একটি টুল তৈরি করেছে। bin
ডিরেক্টরিতে একটি নতুন ফাইল generate_brick_file_names.dart
তৈরি করুন এবং নিম্নলিখিত বিষয়বস্তু যোগ করুন:
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', }, }; }
এই টুলটি সহায়কভাবে স্প্রাইট শীট বিবরণ ফাইলটিকে পার্স করেছে এবং এটিকে ডার্ট কোডে রূপান্তর করেছে যা আমরা প্রতিটি ইটের জন্য সঠিক চিত্র ফাইল নির্বাচন করতে ব্যবহার করতে পারি যা আপনি স্ক্রিনে রাখতে চান। দরকারী!
নিম্নলিখিত বিষয়বস্তু সহ 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
s যোগ করা হচ্ছে একটি এলোমেলো ক্লাস্টারে, সময়ের সাথে সাথে। এর দুটি অংশ রয়েছে, প্রথমটি হল যে পদ্ধতিটি যুক্ত করে Brick
s await
sa Future.delayed
, যা একটি sleep()
কলের অসিঙ্ক্রোনাস সমতুল্য। যাইহোক, এই কাজটি করার জন্য একটি দ্বিতীয় অংশ রয়েছে, onLoad
পদ্ধতিতে addBricks
জন্য কলটি await
করছে না। যদি এটি হয়, সমস্ত ইট পর্দায় না হওয়া পর্যন্ত onLoad
পদ্ধতিটি সম্পূর্ণ হবে না। একটি unawaited
কলে addBricks
কলটি র্যাপ করা লিন্টারদের খুশি করে এবং ভবিষ্যতের প্রোগ্রামারদের কাছে আমাদের উদ্দেশ্যকে স্পষ্ট করে তোলে। এই পদ্ধতি ফিরে আসার জন্য অপেক্ষা না করা ইচ্ছাকৃত।
গেমটি চালান, এবং আপনি দেখতে পাবেন ইট দেখা যাচ্ছে, একে অপরের সাথে ধাক্কা খাচ্ছে এবং মাটিতে ছড়িয়ে পড়ছে।
6. প্লেয়ার যোগ করুন
ইট এ ফ্লিংিং এলিয়েন
প্রথম কয়েকবার ইট টম্বল দেখা মজাদার, কিন্তু আমি অনুমান করছি যে এই গেমটি আরও মজাদার হবে যদি আমরা খেলোয়াড়কে এমন একটি অবতার দেই যে তারা বিশ্বের সাথে যোগাযোগ করতে ব্যবহার করতে পারে। কিভাবে একটি এলিয়েন সম্পর্কে তারা ইট এ উড়ে যেতে পারে?
lib/components
ডিরেক্টরিতে একটি নতুন player.dart
ফাইল তৈরি করুন এবং এতে নিম্নলিখিতগুলি যুক্ত করুন:
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 থেকে এসেছে এবং এটি আপনাকে একটি ক্যানভাসে আঁকতে দেয়। বৃত্তাকার এলিয়েনটি উড়ে যাওয়ার সময় কোথায় উড়ে যাচ্ছে সে সম্পর্কে খেলোয়াড়কে প্রতিক্রিয়া জানাতে এটি এখানে ব্যবহৃত হয়।
কিভাবে প্লেয়ার এলিয়েন এর ফ্লিংিং শুরু করে? একটি টেনে নেওয়ার অঙ্গভঙ্গি ব্যবহার করে, যা প্লেয়ার উপাদানটি DragCallbacks
কলব্যাকগুলির সাথে সনাক্ত করে৷ আপনার মধ্যে ঈগলের চোখ এখানে অন্য কিছু লক্ষ্য করবে।
যেখানে Ground
উপাদানগুলি স্থির দেহ ছিল, ইটের উপাদানগুলি ছিল গতিশীল দেহ। প্লেয়ার এখানে উভয়ের সংমিশ্রণ। প্লেয়ারটি স্ট্যাটিক হিসাবে শুরু হয়, প্লেয়ারের এটি টেনে আনার জন্য অপেক্ষা করে, এবং ড্র্যাগ রিলিজে এটি নিজেকে স্ট্যাটিক থেকে গতিশীলে রূপান্তরিত করে, টেনে আনার অনুপাতে রৈখিক আবেগ যোগ করে এবং এলিয়েন অবতারকে উড়তে দেয়!
Player
কম্পোনেন্টে এটি সীমার বাইরে চলে গেলে, ঘুমিয়ে পড়লে বা সময় শেষ হলে এটিকে স্ক্রীন থেকে সরিয়ে দেওয়ার জন্য কোড রয়েছে। এখানে উদ্দেশ্য হল প্লেয়ারকে এলিয়েন পালানোর অনুমতি দেওয়া, কী হয় তা দেখুন এবং তারপরে আরেকটা যান।
নিম্নলিখিতভাবে game.dart
সম্পাদনা করে Player
উপাদানটিকে গেমের মধ্যে একীভূত করুন:
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. প্রভাব প্রতিক্রিয়া
শত্রুদের যোগ করা
আপনি স্থির এবং গতিশীল বস্তু একে অপরের সাথে যোগাযোগ করতে দেখেছেন। যাইহোক, সত্যিকার অর্থে কোথাও যাওয়ার জন্য, যখন জিনিসগুলি সংঘর্ষ হয় তখন আপনাকে কোডে কলব্যাক পেতে হবে। দেখা যাক কিভাবে করা হয়। প্লেয়ারের বিরুদ্ধে যেতে আপনি কিছু শত্রুর পরিচয় দিতে যাচ্ছেন। এটি একটি বিজয়ী অবস্থার একটি পথ দেয় - গেম থেকে সমস্ত শত্রুদের সরান!
lib/components
ডিরেক্টরিতে একটি enemy.dart
ফাইল তৈরি করুন এবং নিম্নলিখিত যোগ করুন:
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();
}
প্লেয়ার এবং ব্রিক উপাদানগুলির সাথে আপনার পূর্ববর্তী মিথস্ক্রিয়া থেকে, এই ফাইলের বেশিরভাগই পরিচিত হওয়া উচিত। যাইহোক, একটি নতুন অজানা বেস ক্লাসের কারণে আপনার সম্পাদকে কয়েকটি লাল আন্ডারলাইন থাকবে। নিম্নলিখিত বিষয়বস্তু সহ lib/components
এ body_component_with_user_data.dart
নামের একটি ফাইল যোগ করে এখন এই ক্লাসটি যোগ করুন:
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;
}
}
Enemy
কম্পোনেন্টে নতুন beginContact
কলব্যাকের সাথে মিলিত এই বেস ক্লাসটি সংস্থার মধ্যে প্রভাব সম্পর্কে প্রোগ্রাম্যাটিকভাবে বিজ্ঞপ্তি পাওয়ার ভিত্তি তৈরি করে। প্রকৃতপক্ষে, আপনি যে কোনও উপাদানের মধ্যে প্রভাব বিজ্ঞপ্তি পেতে চান তা সম্পাদনা করতে হবে। সুতরাং, এগিয়ে যান এবং 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 গেম ইঞ্জিন ব্যবহার করে একটি গেম তৈরি করেছেন এবং এটি একটি Flutter wrapper এ এমবেড করেছেন৷ আপনি উপাদানগুলিকে অ্যানিমেট করতে এবং অপসারণ করতে শিখার প্রভাবগুলি ব্যবহার করেছেন৷ আপনি Google ফন্ট এবং ফ্লটার অ্যানিমেট প্যাকেজ ব্যবহার করেছেন যাতে পুরো গেমটি ভালভাবে ডিজাইন করা হয়।
এরপর কি?
এই কোডল্যাবগুলির কিছু পরীক্ষা করে দেখুন...
- ফ্লটারে পরবর্তী প্রজন্মের UI তৈরি করা
- আপনার ফ্লটার অ্যাপটিকে বিরক্তিকর থেকে সুন্দরের দিকে নিয়ে যান
- আপনার Flutter অ্যাপে অ্যাপ-মধ্যস্থ কেনাকাটা যোগ করা হচ্ছে