1. शुरू करने से पहले
Flame, Flutter पर आधारित 2D गेम इंजन है. इस कोडलैब में, एक ऐसा गेम बनाया जाता है जो Box2D की लाइनों के साथ-साथ, 2D फ़िज़िक्स सिम्युलेशन का इस्तेमाल करता है. इस गेम को Forge2D कहा जाता है. असली हकीकत को सिम्युलेट करने के लिए, Flame के कॉम्पोनेंट इस्तेमाल किए जाते हैं. इससे आपके उपयोगकर्ता, स्क्रीन पर गेम खेल सकते हैं. पूरा होने पर, आपका गेम इस ऐनिमेटेड GIF की तरह दिखना चाहिए:
ज़रूरी शर्तें
- Flutter की मदद से, Flame के बारे में जानकारी कोडलैब (कोड बनाना सीखना) पूरा हुआ
आपको ये सब सीखने को मिलेगा
- Forge2D की बुनियादी बातें कैसे काम करती हैं? इसकी शुरुआत, अलग-अलग तरह के भौतिक शरीरों से होती है.
- फ़िज़िकल सिम्युलेशन को 2D में कैसे सेट अप करें.
आपको इन चीज़ों की ज़रूरत पड़ेगी
- Flutter का SDK टूल
- Flutter और Dart प्लगिन के साथ विज़ुअल स्टूडियो कोड (वीएस कोड)
आपके चुने गए डेवलपमेंट टारगेट के लिए कंपाइलर सॉफ़्टवेयर. यह कोडलैब, उन सभी छह प्लैटफ़ॉर्म के लिए काम करता है जिन पर Flutter काम करता है. Windows को टारगेट करने के लिए आपको Visual Studio, macOS या iOS को टारगेट करने के लिए Xcode, और Android को टारगेट करने के लिए Android Studio की ज़रूरत होगी.
2. प्रोजेक्ट बनाना
अपना Flutter प्रोजेक्ट बनाएं
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 फ़ंक्शन को इस तरह दिखाता है कि यह फ़्लेम के साथ अच्छी तरह से काम करती है. आखिर में, एक्सएमएल कॉन्टेंट को देखने और उसमें बदलाव करने के लिए, 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 कोड नहीं है जो चल रहे गेम की जानकारी दिखाने के लिए गेम इंस्टेंस की स्थिति का इस्तेमाल करता हो. इसलिए, इस्तेमाल करने में आसान यह बूटस्ट्रैप सही है.
ज़रूरी नहीं: सिर्फ़ 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.
Lame के लिए 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 इमेज और एक्सएमएल फ़ाइल, दोनों के मिले-जुले रूप वाले हैं. इनसे पता चलता है कि स्प्राइटशीट वाली इमेज में छोटी इमेज कहां मिल सकती हैं. स्प्राइटशीट की तकनीक का इस्तेमाल करके, सिर्फ़ एक फ़ाइल लोड करके कॉन्टेंट लोड होने में लगने वाला समय कम किया जा सकता है. अगर एक से ज़्यादा इमेज फ़ाइलें नहीं हों, तो स्प्राइटशीट का इस्तेमाल किया जा सकता है.
अपने प्रोजेक्ट की assets/images
डायरेक्ट्री में, spritesheet_aliens.png
, spritesheet_elements.png
, और spritesheet_tiles.png
के डेटा को कॉपी करें. जब आप यहां हों, तब अपने प्रोजेक्ट की assets
डायरेक्ट्री में spritesheet_aliens.xml
, spritesheet_elements.xml
, और spritesheet_tiles.xml
फ़ाइलें भी कॉपी करें. आपका प्रोजेक्ट ऐसा दिखना चाहिए.
बैकग्राउंड पेंट करें
आपके प्रोजेक्ट में इमेज ऐसेट जोड़ दी गई हैं. इसलिए, अब उन्हें स्क्रीन पर दिखाएं. ठीक है, स्क्रीन पर एक इमेज. चरणों के बारे में ज़्यादा जानकारी, नीचे दिए गए चरणों में मिलेगी.
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
को खास कॉन्फ़िगरेशन की ज़रूरत होती है.
lib/components
डायरेक्ट्री में, एक और नई फ़ाइल बनाएं, जिसका नाम 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';
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 x 600 वर्चुअल पिक्सल पर सेट करके, बैकग्राउंड कॉम्पोनेंट में लगाए गए अनुमान पूरे किए जाते हैं. इसका मतलब है कि गेम एरिया 80 यूनिट चौड़ा और 60 यूनिट लंबा होगा, जिसके बीच में (0,0) यूनिट होगी. इससे स्क्रीन पर दिखाए जा रहे रिज़ॉल्यूशन पर कोई असर नहीं पड़ता. हालांकि, गेम के सीन में ऑब्जेक्ट की जगह पर इसका असर पड़ता है.
camera
कंस्ट्रक्टर तर्क के साथ-साथ, gravity
नाम का एक ज़्यादा भौतिकी वाला अलाइन किया गया तर्क है. ग्रेविटी को Vector2
पर सेट किया गया है, जिसमें 0 का x
और 10 का y
है. 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 x 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
के तौर पर सेट किया जाता है. इस तरह का बॉक्स, दुनिया के ऐक्सिस से अलाइन होता है. यह BoxXY
के रूप में सेट किए गए PolygonShape
के बिलकुल उलट होता है, जिसे घूमने के किसी पॉइंट पर घुमाया जा सकता है. यह फिर से काम का है, लेकिन इस कोडलैब के दायरे से बाहर भी है. आकार और शरीर को एक फ़िक्स्चर के साथ जोड़ा जाता है, जो 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
लूप का इस्तेमाल करके, Ground
कॉम्पोनेंट की सीरीज़ जोड़ता है. इसके बाद, यह Ground
कॉम्पोनेंट की सूची को world
के addAll
तरीके में पास करता है.
गेम को चलाने से अब बैकग्राउंड और मैदान दिख रहा है.
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
के बाद और 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
के await
को Future.delayed
जोड़ने का तरीका है. यह तरीका, एसिंक्रोनस कॉल sleep()
कॉल के बराबर होता है. हालांकि, ऐसा करने का दूसरा तरीका है, onLoad
तरीके में addBricks
को किया गया कॉल await
नहीं किया गया है. अगर ऐसा होता, तो onLoad
वाला तरीका तब तक पूरा नहीं होता, जब तक सभी ब्रिक स्क्रीन पर नहीं आ जाते. addBricks
को unawaited
कॉल में रैप करना लिंटर को खुश कर देता है और भविष्य के प्रोग्रामर के लिए हमारा उद्देश्य स्पष्ट करता है. इस तरीके के वापस आने का इंतज़ार न करना जान-बूझकर किया गया है.
गेम को चलाएं और आपको ब्रिक्स एक-दूसरे से टकराते हुए और ज़मीन पर गिरते हुए दिखाई देंगे.
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
कॉम्पोनेंट मौजूद नहीं है या नहीं. अगर गेम में कोई 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();
}
प्लेयर और ब्रिक कॉम्पोनेंट के साथ आपके पिछले इंटरैक्शन से, इस फ़ाइल का ज़्यादातर हिस्सा जाना-पहचाना होना चाहिए. हालांकि, नए बेस क्लास की वजह से आपके एडिटर में कुछ लाल अंडरलाइन दिखेंगे. 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;
}
}
यह बेस क्लास, Enemy
कॉम्पोनेंट में नए beginContact
कॉलबैक के साथ मिलकर, बॉडी के बीच होने वाले असर के बारे में प्रोग्राम के हिसाब से सूचना पाने का आधार बनाती है. असल में, आपको उन सभी कॉम्पोनेंट में बदलाव करने होंगे जिनके बीच आपको असर के बारे में सूचनाएं चाहिए. इसलिए, BodyComponentWithUserData
का इस्तेमाल करने के लिए, Brick
, Ground
, और Player
कॉम्पोनेंट में बदलाव करें. यह बदलाव, 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. बधाई हो
बधाई हो, आपने Flutter और Flame के साथ गेम बनाने में कामयाब रहे!
आपने Fire 2D गेम इंजन का इस्तेमाल करके गेम बनाया है और उसे Flutter रैपर में एम्बेड किया है. आपने कॉम्पोनेंट को ऐनिमेट और हटाने के लिए, फ़्लेम इफ़ेक्ट का इस्तेमाल किया है. पूरे गेम को अच्छी तरह से डिज़ाइन करने के लिए आपने Google Fonts and Flutter Animate पैकेज का इस्तेमाल किया है.
आगे क्या करना है?
इनमें से कुछ कोडलैब देखें...
- Flutter में अगली-पीढ़ी की टेक्नोलॉजी के यूआई बनाना
- Flutter ऐप्लिकेशन को बोरिंग से खूबसूरत बनाएं
- अपने Flutter ऐप्लिकेशन में इन-ऐप्लिकेशन खरीदारी जोड़ना