Flutter और Flame के साथ 2D फ़िज़िक्स गेम बनाएं

1. शुरू करने से पहले

Flame, Flutter पर आधारित 2D गेम इंजन है. इस कोडलैब में, एक ऐसा गेम बनाया जाता है जो Box2D की लाइनों के साथ-साथ, 2D फ़िज़िक्स सिम्युलेशन का इस्तेमाल करता है. इस गेम को Forge2D कहा जाता है. असली हकीकत को सिम्युलेट करने के लिए, Flame के कॉम्पोनेंट इस्तेमाल किए जाते हैं. इससे आपके उपयोगकर्ता, स्क्रीन पर गेम खेल सकते हैं. पूरा होने पर, आपका गेम इस ऐनिमेटेड GIF की तरह दिखना चाहिए:

इस 2D फ़िज़िक्स गेम में गेम खेलने का ऐनिमेशन

ज़रूरी शर्तें

आपको ये सब सीखने को मिलेगा

  • Forge2D की बुनियादी बातें कैसे काम करती हैं? इसकी शुरुआत, अलग-अलग तरह के भौतिक शरीरों से होती है.
  • फ़िज़िकल सिम्युलेशन को 2D में कैसे सेट अप करें.

आपको इन चीज़ों की ज़रूरत पड़ेगी

आपके चुने गए डेवलपमेंट टारगेट के लिए कंपाइलर सॉफ़्टवेयर. यह कोडलैब, उन सभी छह प्लैटफ़ॉर्म के लिए काम करता है जिन पर Flutter काम करता है. Windows को टारगेट करने के लिए आपको Visual Studio, macOS या iOS को टारगेट करने के लिए Xcode, और Android को टारगेट करने के लिए Android Studio की ज़रूरत होगी.

2. प्रोजेक्ट बनाना

अपना Flutter प्रोजेक्ट बनाएं

Flutter प्रोजेक्ट बनाने के कई तरीके हैं. इस सेक्शन में, कम शब्दों में जानकारी देने के लिए कमांड लाइन का इस्तेमाल किया गया है.

शुरू करने के लिए, इन चरणों का पालन करें:

  1. कमांड लाइन पर, 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.
  1. प्रोजेक्ट की डिपेंडेंसी में बदलाव करके, 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 रनर के प्रोजेक्ट कॉन्फ़िगरेशन में बदलाव किया जा सकता है, ताकि टाइटल बार से आगे बढ़ा जा सके.

ऐसा करने के लिए, इन चरणों का अनुसरण करें:

  1. कोई 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 डायरेक्ट्री में नहीं है, क्योंकि यह गेम के रनटाइम कोड बेस का हिस्सा नहीं है. यह कमांड लाइन टूल, जिसका इस्तेमाल प्रोजेक्ट में बदलाव करने के लिए किया जाता है.

  1. प्रोजेक्ट बेस डायरेक्ट्री से, टूल को इस तरह से चलाएं:
$ 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/बैकग्राउंड डायरेक्ट्री को हाइलाइट किया गया है

PNG/Backgrounds डायरेक्ट्री से, colored_desert.png, colored_grass.png, colored_land.png, और colored_shroom.png फ़ाइलों को अपने प्रोजेक्ट की assets/images डायरेक्ट्री में कॉपी करें.

यहां स्प्राइट शीट भी है. ये PNG इमेज और एक्सएमएल फ़ाइल, दोनों के मिले-जुले रूप वाले हैं. इनसे पता चलता है कि स्प्राइटशीट वाली इमेज में छोटी इमेज कहां मिल सकती हैं. स्प्राइटशीट की तकनीक का इस्तेमाल करके, सिर्फ़ एक फ़ाइल लोड करके कॉन्टेंट लोड होने में लगने वाला समय कम किया जा सकता है. अगर एक से ज़्यादा इमेज फ़ाइलें नहीं हों, तो स्प्राइटशीट का इस्तेमाल किया जा सकता है.

केनी_फ़िज़िक्स-ऐसेट पैक की फ़ाइल की सूची को बड़ा किया गया. इसमें Spritesheet की डायरेक्ट्री को हाइलाइट किया गया है

अपने प्रोजेक्ट की assets/images डायरेक्ट्री में, spritesheet_aliens.png, spritesheet_elements.png, और spritesheet_tiles.png के डेटा को कॉपी करें. जब आप यहां हों, तब अपने प्रोजेक्ट की assets डायरेक्ट्री में spritesheet_aliens.xml, spritesheet_elements.xml, और spritesheet_tiles.xml फ़ाइलें भी कॉपी करें. आपका प्रोजेक्ट ऐसा दिखना चाहिए.

forge2d_game प्रोजेक्ट डायरेक्ट्री की फ़ाइल में, ऐसेट की डायरेक्ट्री को हाइलाइट किया गया है

बैकग्राउंड पेंट करें

आपके प्रोजेक्ट में इमेज ऐसेट जोड़ दी गई हैं. इसलिए, अब उन्हें स्क्रीन पर दिखाएं. ठीक है, स्क्रीन पर एक इमेज. चरणों के बारे में ज़्यादा जानकारी, नीचे दिए गए चरणों में मिलेगी.

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.
  }
}

आपके पास यह चुनौती है कि आप गेम को चलाकर खुद को इस स्क्रीन पर लगाकर स्वीकार कर लें.

एक ऐप्लिकेशन की विंडो, जिसके बैकग्राउंड में हरी-भरी पहाड़ियां हैं. साथ ही, ग्राउंड लेयर, ग्राउंड पर ब्लॉक, और &#39;आप जीत गए!&#39; टेक्स्ट ओवरले है

8. बधाई हो

बधाई हो, आपने Flutter और Flame के साथ गेम बनाने में कामयाब रहे!

आपने Fire 2D गेम इंजन का इस्तेमाल करके गेम बनाया है और उसे Flutter रैपर में एम्बेड किया है. आपने कॉम्पोनेंट को ऐनिमेट और हटाने के लिए, फ़्लेम इफ़ेक्ट का इस्तेमाल किया है. पूरे गेम को अच्छी तरह से डिज़ाइन करने के लिए आपने Google Fonts and Flutter Animate पैकेज का इस्तेमाल किया है.

आगे क्या करना है?

इनमें से कुछ कोडलैब देखें...

इसके बारे में और पढ़ें