Flutter के साथ फ़्लेम के बारे में जानकारी

1. परिचय

Flame, Flutter पर आधारित 2D गेम इंजन है. इस कोडलैब में आपको एक गेम बनाना है, जो 70 के दशक के क्लासिक वीडियो गेम में से एक, स्टीव वॉज़्निएक के ब्रेकआउट से प्रेरित होगा. चमगादड़, बॉल, और ईंटें बनाने के लिए, आपको फ़्लेम के कॉम्पोनेंट इस्तेमाल करने होंगे. चमगादड़ की गतिविधि को ऐनिमेट करने के लिए, आप Flame के इफ़ेक्ट का इस्तेमाल करेंगे और यह देखेंगे कि Flutter के स्टेट मैनेजमेंट सिस्टम के साथ Flame को कैसे इंटिग्रेट किया जाए.

गेम पूरा होने पर, आपका गेम इस ऐनिमेटेड GIF की तरह दिखना चाहिए. भले ही, यह थोड़ा धीमा हो जाए.

खेले जा रहे गेम की स्क्रीन रिकॉर्डिंग. गेम में तेज़ी से तेज़ी आ गई है.

आप इन चीज़ों के बारे में जानेंगे

  • फ़्लेम की बुनियादी बातें कैसे काम करती हैं, इसकी शुरुआत GameWidget से होती है.
  • गेम लूप इस्तेमाल करने का तरीका.
  • फ़्लेम की Component कैसे काम करती है. वे Flutter के Widget से मिलते-जुलते हैं.
  • टकरावों को कैसे हैंडल करें.
  • Component को ऐनिमेट करने के लिए Effect का इस्तेमाल करने का तरीका.
  • Flutter Widgets को Flame गेम पर ओवरले करने का तरीका.
  • Flutter के स्टेट मैनेजमेंट के साथ Flame को इंटिग्रेट करने का तरीका.

आपको क्या बनाना होगा

इस कोडलैब में, Flutter और Flame का इस्तेमाल करके 2D गेम बनाया जा सकता है. गेम पूरा होने पर, आपके गेम को ये ज़रूरी शर्तें पूरी करनी होंगी

  • Flutter के साथ काम करने वाले सभी छह प्लैटफ़ॉर्म पर फ़ंक्शन मौजूद है: Android, iOS, Linux, macOS, Windows, और वेब
  • Flame के गेम लूप का इस्तेमाल करके, कम से कम 60 FPS (फ़्रेम प्रति सेकंड) बनाए रखें.
  • google_fonts पैकेज और flutter_animate जैसी Flutter सुविधाओं का इस्तेमाल करके, 80 के दशक के आर्केड गेमिंग का आनंद फिर से लें.

2. Flutter एनवायरमेंट को सेट अप करें

संपादक

इस कोडलैब को आसान बनाने के लिए, यह माना जाता है कि विज़ुअल स्टूडियो कोड (वीएस कोड) आपका डेवलपमेंट एनवायरमेंट है. बनाम कोड के लिए कोई शुल्क नहीं लिया जाता और यह सभी बड़े प्लैटफ़ॉर्म पर काम करता है. हम इस कोडलैब के लिए वीएस कोड का इस्तेमाल करते हैं, क्योंकि निर्देशों में डिफ़ॉल्ट रूप से वीएस कोड के हिसाब से बने शॉर्टकट होते हैं. अब टास्क और भी आसान हो जाते हैं: "इस बटन पर क्लिक करें" या "X करने के लिए यह बटन दबाएं" के बजाय "X करने के लिए अपने संपादक में उचित कार्रवाई करें" का उपयोग करें.

अपनी पसंद के किसी भी एडिटर का इस्तेमाल किया जा सकता है: Android Studio, अन्य IntelliJ IDEs, Emacs, Vim या Notepad++. वे सभी Flutter के साथ काम करते हैं.

कुछ Flutter कोड के साथ VS कोड का स्क्रीनशॉट

डेवलपमेंट टारगेट चुनें

Flutter इस्तेमाल करें और कई प्लैटफ़ॉर्म के लिए ऐप्लिकेशन बनाएं. आपका ऐप्लिकेशन इनमें से किसी भी ऑपरेटिंग सिस्टम पर चल सकता है:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • वेब

अपने डेवलपमेंट टारगेट के तौर पर किसी एक ऑपरेटिंग सिस्टम को चुनना आम बात है. यह वह ऑपरेटिंग सिस्टम है जिस पर आपका ऐप्लिकेशन डेवलपमेंट के दौरान चलता है.

एक ड्रॉइंग, जिसमें लैपटॉप और फ़ोन को केबल की मदद से जोड़े गए फ़ोन को दिखाया गया है. लैपटॉप

उदाहरण के लिए: मान लें कि अपना Flutter ऐप्लिकेशन डेवलप करने के लिए Windows लैपटॉप का इस्तेमाल किया जा रहा है. इसके बाद, डेवलपमेंट टारगेट के तौर पर Android को चुना जाता है. अपने ऐप्लिकेशन की झलक देखने के लिए, किसी Android डिवाइस को यूएसबी केबल की मदद से अपने Windows लैपटॉप से जोड़ा जाता है. इसके बाद, आपका ऐप्लिकेशन जिस Android डिवाइस से अटैच किया गया है या Android एम्युलेटर पर चलता है. आप डेवलपमेंट टारगेट के तौर पर Windows को चुन सकते थे, जो आपके ऐप्लिकेशन के डेवलपमेंट में, एडिटर के साथ-साथ Windows ऐप्लिकेशन के तौर पर भी काम करता था.

आप शायद वेब को अपने डेवलपमेंट टारगेट के तौर पर चुनना चाहें. डेवलपमेंट के दौरान इसकी एक समस्या है: Flutter की Stateful Hot Reload सुविधा ऐक्सेस नहीं की जा सकती. Flutter फ़िलहाल वेब ऐप्लिकेशन को हॉट-रीलोड नहीं कर सकता.

जारी रखने से पहले अपनी पसंद तय करें. अन्य ऑपरेटिंग सिस्टम पर अपने ऐप्लिकेशन को बाद में कभी भी चलाया जा सकता है. डेवलपमेंट टारगेट चुनने पर, अगला चरण आसान हो जाता है.

Flutter इंस्टॉल करें

Flutter SDK टूल इंस्टॉल करने के अप-टू-डेट निर्देश docs.flutter.dev पर मिल सकते हैं.

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

  1. Flutter SDK टूल
  2. Flutter प्लगिन के साथ विज़ुअल स्टूडियो कोड
  3. आपके चुने गए डेवलपमेंट टारगेट के लिए कंपाइलर सॉफ़्टवेयर. (macOS या iOS को टारगेट करने के लिए, आपको Windows या Xcode को टारगेट करने के लिए Visual Studio की ज़रूरत होगी)

अगले सेक्शन में, आपको अपना पहला Flutter प्रोजेक्ट बनाना होगा.

अगर आपको किसी समस्या को हल करना है, तो इनमें से कुछ सवाल और जवाब (StackOverflow से) आपको मिल सकते हैं.

अक्सर पूछे जाने वाले सवाल

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

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

इसमें, VS कोड को खोलना और अपनी चुनी गई डायरेक्ट्री में Flutter ऐप्लिकेशन टेंप्लेट बनाना शामिल है.

  1. विज़ुअल स्टूडियो कोड लॉन्च करें.
  2. कमांड पटल (F1 या Ctrl+Shift+P या Shift+Cmd+P) खोलें. इसके बाद, "fltter new" टाइप करें. स्क्रीन पर दिखने पर, Flutter: नया प्रोजेक्ट कमांड चुनें.

बनाम स्क्रीनशॉट

  1. ऐप्लिकेशन खाली करें चुनें. वह डायरेक्ट्री चुनें जिसमें आपको प्रोजेक्ट बनाना है. यह कोई भी ऐसी डायरेक्ट्री होनी चाहिए जिसके लिए खास सुविधाओं की ज़रूरत न हो या पाथ में कोई स्पेस न हो. उदाहरण के लिए, आपकी होम डायरेक्ट्री या C:\src\.

खाली ऐप्लिकेशन वाले वीएस कोड का स्क्रीनशॉट, जिसे नए ऐप्लिकेशन फ़्लो के हिस्से के तौर पर चुना गया है

  1. अपने प्रोजेक्ट को brick_breaker नाम दें. इस कोडलैब के बाकी हिस्से को यह मानकर चलता है कि आपने अपने ऐप्लिकेशन का नाम brick_breaker रखा है.

वीएस कोड का स्क्रीन शॉट

Flutter अब आपका प्रोजेक्ट फ़ोल्डर बनाता है और VS Code खोलता है. अब आप ऐप्लिकेशन के बुनियादी स्कैफ़ोल्ड से दो फ़ाइलों की सामग्री को ओवरराइट कर देंगे.

कॉपी करें और शुरुआती ऐप्लिकेशन चिपकाएं

ऐसा करने से, इस कोडलैब में दिया गया कोड आपके ऐप्लिकेशन में जुड़ जाता है.

  1. वीएस कोड के बाएं पैनल में, Explorer पर क्लिक करें और pubspec.yaml फ़ाइल खोलें.

वीएस कोड का कुछ स्क्रीन शॉट, जिसमें pubspec.yaml फ़ाइल की जगह को हाइलाइट किया गया है

  1. इस फ़ाइल की सामग्री को इनसे बदलें:

pubspec.yaml

name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.3.0 <4.0.0'

dependencies:
  flame: ^1.16.0
  flutter:
    sdk: flutter
  flutter_animate: ^4.5.0
  google_fonts: ^6.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.1

flutter:
  uses-material-design: true

pubspec.yaml फ़ाइल आपके ऐप्लिकेशन के बारे में बुनियादी जानकारी के बारे में बताती है. जैसे, ऐप्लिकेशन का मौजूदा वर्शन, उसकी डिपेंडेंसी, और वे एसेट जिनसे ऐप्लिकेशन शिप किया जाएगा.

  1. main.dart फ़ाइल को lib/ डायरेक्ट्री में खोलें.

VS कोड का आंशिक स्क्रीन शॉट, जिसमें Main.dart फ़ाइल की जगह को दिखाया गया है

  1. इस फ़ाइल की सामग्री को इनसे बदलें:

lib/main.dart

import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() {
  final game = FlameGame();
  runApp(GameWidget(game: game));
}
  1. सब कुछ काम कर रहा है, इसकी पुष्टि करने के लिए इस कोड को चलाएं. इसमें एक नई विंडो दिखाई देगी, जिसमें सिर्फ़ खाली काला बैकग्राउंड होगा. दुनिया का सबसे खराब वीडियो गेम अब 60fps पर रेंडर हो रहा है!

पूरी तरह से काले रंग की ब्रिक_breaker ऐप्लिकेशन विंडो को दिखाने वाला स्क्रीन शॉट.

4. गेम बनाएं

गेम का साइज़ बढ़ाएं

दो डाइमेंशन (2D) में खेले जाने वाले गेम के लिए, खेलने की जगह की ज़रूरत होती है. आपको खास डाइमेंशन का एक एरिया बनाना होगा. इसके बाद, इन डाइमेंशन का इस्तेमाल गेम के दूसरे आसपेक्ट को साइज़ देने के लिए किया जाएगा.

खेल की जगह में निर्देशांक बनाने के कई तरीके हैं. एक तरीके से, स्क्रीन के बीच से दिशा को मापा जा सकता है. ऐसा ऑरिजिन (0,0)स्क्रीन के बीच में होता है. पॉज़िटिव वैल्यू आइटम को x ऐक्सिस पर दाईं ओर और y ऐक्सिस पर ऊपर की ओर ले जाती हैं. यह मानक इन दिनों ज़्यादातर मौजूदा गेम पर लागू होता है, खासकर तब, जब थ्री डाइमेंशन वाले गेम होते हैं.

मूल ब्रेकआउट गेम बनाए जाने के दौरान, सबसे पहले बाएं कोने में ऑरिजिन सेट किया गया था. पॉज़िटिव x दिशा में कोई बदलाव नहीं हुआ है. हालांकि, y को फ़्लिप किया गया है. x सकारात्मक x दिशा सही थी और y नीचे थी. उस दौर के हिसाब से बने रहने के लिए, यह गेम ऑरिजिन को सबसे ऊपर बाएं कोने पर सेट करता है.

lib/src नाम की नई डायरेक्ट्री में config.dart नाम की फ़ाइल बनाएं. इस फ़ाइल में, नीचे दिए गए तरीके के हिसाब से ज़्यादा कॉन्सटेंट मिलेंगे.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;

इस गेम की चौड़ाई 820 पिक्सल और ऊंचाई 1600 पिक्सल होगी. गेम एरिया उस विंडो में फ़िट हो जाता है जिसमें वह दिखता है, लेकिन स्क्रीन पर जोड़े गए सभी कॉम्पोनेंट, इस ऊंचाई और चौड़ाई के मुताबिक होते हैं.

PlayArea बनाएं

ब्रेकआउट गेम में, बॉल खेल की जगह की दीवारों से उछलती है. टकरावों को मैनेज करने के लिए, आपको पहले PlayArea कॉम्पोनेंट की ज़रूरत होगी.

  1. lib/src/components नाम की नई डायरेक्ट्री में play_area.dart नाम की फ़ाइल बनाएं.
  2. इस फ़ाइल में इन्हें जोड़ें.

lib/src/components/play_area.dart

import 'dart:async';

import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';

class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
  PlayArea()
      : super(
          paint: Paint()..color = const Color(0xfff2e8cf),
        );

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}

जहां Flutter में Widgets हैं, वहीं Lame में Components हैं. Flutter ऐप्लिकेशन में विजेट के ट्री बनाए जाते हैं, जबकि फ़्लेम गेम में अलग-अलग कॉम्पोनेंट के पेड़ों का रखरखाव किया जाता है.

फ़्लटर और फ़्लेम के बीच दिलचस्प अंतर है. Flutter का विजेट ट्री, कुछ समय के लिए बनाया जाता है. इसका इस्तेमाल, स्थायी और बदलाव किए जा सकने वाले RenderObject लेयर को अपडेट करने के लिए किया जाता है. फ़्लेम के कॉम्पोनेंट स्थायी होते हैं और उनमें बदलाव किया जा सकता है. इसलिए, यह माना जाता है कि डेवलपर, सिम्युलेशन सिस्टम के हिस्से के तौर पर इन कॉम्पोनेंट का इस्तेमाल करेगा.

फ़्लेम के कॉम्पोनेंट, गेम को बेहतर तरीके से दिखाने के लिए ऑप्टिमाइज़ किए जाते हैं. यह कोडलैब, गेम लूप से शुरू होगा. इसे अगले चरण में दिखाया जाएगा.

  1. ग़ैर-ज़रूरी चीज़ों को कंट्रोल करने के लिए, कोई ऐसी फ़ाइल जोड़ें जिसमें इस प्रोजेक्ट के सभी कॉम्पोनेंट शामिल हों. lib/src/components में components.dart फ़ाइल बनाएं और यह कॉन्टेंट जोड़ें.

lib/src/components/components.dart

export 'play_area.dart';

export डायरेक्टिव, import की इन्वर्स भूमिका निभाता है. यह बताता है कि किसी दूसरी फ़ाइल में इंपोर्ट करने पर, यह फ़ाइल किस फ़ंक्शन से जुड़ी जानकारी दिखाती है. नीचे दिए गए चरणों में नए कॉम्पोनेंट जोड़ने पर, इस फ़ाइल में एंट्री बढ़ जाएंगी.

एक Flame गेम बनाएं

पिछले चरण से लाल रंग के स्क्विगल को दूर करने के लिए, फ़्लेम की FlameGame के लिए एक नई सब-क्लास बनाएं.

  1. lib/src में brick_breaker.dart नाम की फ़ाइल बनाएं और यह कोड जोड़ें.

lib/src/brick_breaker.dart

import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame/game.dart';

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame {
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());
  }
}

यह फ़ाइल गेम की कार्रवाइयों को कंट्रोल करती है. गेम इंस्टेंस बनाते समय, यह कोड गेम को फ़िक्स्ड रिज़ॉल्यूशन रेंडरिंग का इस्तेमाल करने के लिए कॉन्फ़िगर करता है. गेम, स्क्रीन पर दिखने वाली स्क्रीन के साइज़ में बदलाव करता है और ज़रूरत के हिसाब से लेटरबॉक्स का इस्तेमाल करता है.

गेम की चौड़ाई और ऊंचाई को बिना अनुमति के सार्वजनिक किया जाता है, ताकि PlayArea जैसे चाइल्ड कॉम्पोनेंट खुद को सही साइज़ पर सेट कर सकें.

ओवरराइड किए गए onLoad तरीके में, आपका कोड दो कार्रवाइयां करता है.

  1. व्यूफ़ाइंडर के लिए ऐंकर के तौर पर सबसे ऊपर बाईं ओर कॉन्फ़िगर करता है. डिफ़ॉल्ट रूप से, व्यूफ़ाइंडर (0,0) के लिए ऐंकर के तौर पर एरिया के बीच का इस्तेमाल करता है.
  2. PlayArea को world में जोड़ता है. दुनिया, गेम की दुनिया का प्रतिनिधित्व करती है. यह अपने सभी बच्चों को, CameraComponent के ट्रांसफ़ॉर्मेशन व्यू के ज़रिए दिखाता है.

गेम को स्क्रीन पर दिखाएं

इस चरण में किए गए सभी बदलाव देखने के लिए, अपनी lib/main.dart फ़ाइल में ये बदलाव करें.

lib/main.dart

import 'package:flame/game.dart';
import 'package:flutter/material.dart';

import 'src/brick_breaker.dart';                                // Add this import

void main() {
  final game = BrickBreaker();                                  // Modify this line
  runApp(GameWidget(game: game));
}

ये बदलाव करने के बाद, गेम फिर से शुरू करें. गेम नीचे दिए गए डायग्राम में दिखना चाहिए.

ऐप्लिकेशन विंडो के बीच में, रेत के रंग का रेक्टैंगल और brick_breaker ऐप्लिकेशन विंडो दिखाने वाला स्क्रीन शॉट

अगले चरण में, आपको दुनिया के सामने एक बॉल जोड़नी होगी और उसे हिलाना होगा!

5. बॉल दिखाएं

बॉल कॉम्पोनेंट बनाना

स्क्रीन पर एक हिलती हुई बॉल को दिखाने के लिए एक और कॉम्पोनेंट बनाना और उसे गेम की दुनिया में शामिल करना ज़रूरी है.

  1. lib/src/config.dart फ़ाइल के कॉन्टेंट में, दिए गए तरीके का इस्तेमाल करके बदलाव करें.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;                            // Add this constant

इस कोडलैब में, कॉन्सटेंट को डिराइव्ड वैल्यू के तौर पर तय करने वाला डिज़ाइन पैटर्न कई बार दिखेगा. इससे आपको टॉप लेवल gameWidth और gameHeight में बदलाव करने की सुविधा मिलती है. इससे, यह पता लगाया जा सकता है कि गेम कैसा दिखता है और उसमें क्या बदलाव होते हैं.

  1. lib/src/components की ball.dart नाम वाली फ़ाइल में, Ball कॉम्पोनेंट बनाएं.

lib/src/components/ball.dart

import 'package:flame/components.dart';
import 'package:flutter/material.dart';

class Ball extends CircleComponent {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill);

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }
}

पहले, आपने RectangleComponent का इस्तेमाल करके PlayArea तय किया था, इसलिए इससे पता चलता है कि अन्य आकृतियां भी मौजूद हैं. RectangleComponent की तरह, CircleComponent को PositionedComponent से लिया गया है, ताकि आप स्क्रीन पर बॉल को रख सकें. इससे भी अहम बात यह है कि इसकी जगह को अपडेट किया जा सकता है.

यह कॉम्पोनेंट, velocity के बारे में जानकारी देता है या समय के साथ इसकी जगह बदलता है. वेलोसिटी एक Vector2 ऑब्जेक्ट है, क्योंकि वेलोसिटी, स्पीड और दिशा, दोनों है. पोज़िशन अपडेट करने के लिए, update तरीका बदलें. गेम इंजन, हर फ़्रेम के लिए इस तरीके का इस्तेमाल करता है. dt पिछले फ़्रेम और इस फ़्रेम के बीच की अवधि है. इसकी मदद से, बहुत ज़्यादा कंप्यूटेशन (हिसाब लगाना) की वजह से, अलग-अलग फ़्रेम रेट (60 हर्ट्ज़ या 120 हर्ट्ज़) या लंबे फ़्रेम जैसी चीज़ों के हिसाब से खुद को ढाला जा सकता है.

position += velocity * dt के अपडेट पर खास ध्यान दें. इस तरह आप समय के साथ, मोशन के अलग-अलग सिम्युलेशन को अपडेट करने का तरीका लागू करते हैं.

  1. कॉम्पोनेंट की सूची में Ball कॉम्पोनेंट को शामिल करने के लिए, lib/src/components/components.dart फ़ाइल में नीचे बताए गए तरीके से बदलाव करें.

lib/src/components/components.dart

export 'ball.dart';                           // Add this export
export 'play_area.dart';

दुनिया में गेंद का आनंद लेना

तुम्हारे पास गेंद है. आइए, इसे दुनिया में जगह पर रखते हैं और खेल की जगह में इधर-उधर जाने के लिए इसे सेट अप करते हैं.

lib/src/brick_breaker.dart फ़ाइल में, दिए गए तरीके से बदलाव करें.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math; // Add this import

import 'package:flame/components.dart';
import 'package:flame/game.dart';

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame {
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();                                   // Add this variable
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(                                             // Add from here...
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    debugMode = true;                                           // To here.
  }
}

इस बदलाव से, Ball कॉम्पोनेंट को world में जोड़ दिया जाता है. बॉल के position को डिसप्ले एरिया के बीच में सेट करने के लिए, कोड पहले गेम के साइज़ को आधा कर देता है. ऐसा इसलिए, क्योंकि Vector2 में ऑपरेटर ओवरलोड (* और /) होते हैं, ताकि Vector2 को अदिश मान से स्केल किया जा सके.

बॉल के velocity को सेट करने में ज़्यादा मुश्किल होना ज़रूरी है. इसका मकसद बॉल को सही रफ़्तार पर स्क्रीन पर किसी भी क्रम में लगाना है. normalized तरीके को कॉल करने पर, एक Vector2 ऑब्जेक्ट बनता है. यह ऑब्जेक्ट, मूल Vector2 की दिशा पर सेट होता है, लेकिन इसका स्केल 1 होता है. इससे बॉल की रफ़्तार एक जैसी रहती है, भले ही बॉल किसी भी दिशा में जाए. इसके बाद, बॉल की रफ़्तार को खेल की ऊंचाई के 1/4 तक बढ़ाया जाता है.

इन अलग-अलग वैल्यू को सही तरीके से हासिल करने के लिए बार-बार बदलाव करना पड़ता है. इसे इंडस्ट्री में प्लेटेस्टिंग भी कहा जाता है.

आखिरी लाइन, डीबगिंग डिसप्ले को चालू कर देती है. इससे डिसप्ले में और जानकारी जुड़ जाती है, ताकि डीबग करने में मदद मिल सके.

अब गेम चलाने पर, यह निम्न डिस्प्ले जैसा दिखना चाहिए.

एक स्क्रीन शॉट, जिसमें ब्रिक_ब्रेकर ऐप्लिकेशन विंडो को दिखाया गया है. साथ ही, रेत से भरे रेक्टैंगल के ऊपर नीले रंग का सर्कल दिख रहा है. नीले रंग के गोले के ऊपर संख्याएं दिख रही हैं, जिनसे पता चलता है कि स्क्रीन पर इसका साइज़ और जगह है

PlayArea कॉम्पोनेंट और Ball कॉम्पोनेंट, दोनों में डीबग करने की जानकारी मौजूद है. हालांकि, बैकग्राउंड मैट, PlayArea के नंबर को काट देते हैं. हर चीज़ में डीबग करने की जानकारी दिखने की वजह यह है कि आपने पूरे कॉम्पोनेंट ट्री के लिए debugMode को चालू किया है. अगर यह ज़्यादा काम का है, तो सिर्फ़ चुने गए कॉम्पोनेंट के लिए भी डीबग करने की सुविधा चालू की जा सकती है.

गेम को कई बार रीस्टार्ट करने पर, ऐसा हो सकता है कि बॉल उम्मीद के मुताबिक दीवारों से इंटरैक्ट न करती हो. उस प्रभाव को पूरा करने के लिए, आपको टक्कर का पता लगाने की सुविधा को जोड़ना होगा, जो आपको अगले चरण में करना होगा.

6. बाउंसिंग इफ़ेक्ट

टक्कर का पता लगाने की सुविधा जोड़ें

टक्कर का पता लगाने की सुविधा से, आपका गेम इस बात की पहचान कर लेता है कि दो ऑब्जेक्ट एक-दूसरे के संपर्क में आए.

गेम में टक्कर लगने का पता लगाने की सुविधा जोड़ने के लिए, BrickBreaker गेम में HasCollisionDetection मिक्सिन जोड़ें, जैसा कि इस कोड में दिखाया गया है.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/game.dart';

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    debugMode = true;
  }
}

यह कॉम्पोनेंट के हिटबॉक्स को ट्रैक करता है और हर गेम टिक पर कोलिज़न कॉलबैक ट्रिगर करता है.

गेम के हिटबॉक्स की जानकारी अपने-आप भरने के लिए, नीचे बताए गए तरीके से PlayArea कॉम्पोनेंट में बदलाव करें.

lib/src/components/play_area.dart

import 'dart:async';

import 'package:flame/collisions.dart';                         // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';

class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
  PlayArea()
      : super(
          paint: Paint()..color = const Color(0xfff2e8cf),
          children: [RectangleHitbox()],                        // Add this parameter
        );

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}

RectangleComponent के चाइल्ड के रूप में RectangleHitbox कॉम्पोनेंट जोड़ने से, टक्कर का पता लगाने के लिए एक हिट बॉक्स बनेगा, जो पैरंट कॉम्पोनेंट के साइज़ से मेल खाता है. कई बार RectangleHitbox के लिए relative नाम का एक फ़ैक्ट्री कंस्ट्रक्टर मौजूद है, जब आपको ऐसा हिटबॉक्स चाहिए जो पैरंट कॉम्पोनेंट से छोटा या बड़ा हो.

बॉल बाउंस करें

अब तक, टक्कर का पता लगाने की सुविधा जोड़ने से गेमप्ले पर कोई असर नहीं पड़ा है. Ball कॉम्पोनेंट में बदलाव करने के बाद, यह बदलाव होता है. जब बॉल PlayArea से टकराती है, तब इसका व्यवहार भी बदलता है.

Ball कॉम्पोनेंट में नीचे बताए गए तरीके से बदलाव करें.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';                         // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';                                 // And this import
import 'play_area.dart';                                        // And this one too

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {   // Add these mixins
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);                        // Add this parameter

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override                                                     // Add from here...
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        removeFromParent();
      }
    } else {
      debugPrint('collision with $other');
    }
  }                                                             // To here.
}

यह उदाहरण, onCollisionStart कॉलबैक को जोड़ने के साथ एक बड़ा बदलाव करता है. पिछले उदाहरण में BrickBreaker में जोड़ा गया टकराव की पहचान करने वाला सिस्टम इस कॉलबैक को कॉल करता है.

सबसे पहले, कोड यह जांच करता है कि Ball और PlayArea के बीच क्या टकराव हुआ है. फ़िलहाल, ऐसा लगता है कि इस गेम में कोई और कॉम्पोनेंट नहीं है. यह अगले चरण में बदल जाएगा, जब आप दुनिया के लिए एक बैट जोड़ेंगे. इसके बाद, जब बॉल किसी ऐसी चीज़ से टकराती है जो बल्ले की नहीं है, तो हैंडल करने के लिए else शर्त भी जोड़ी जाती है. बाकी लॉजिक को लागू करने के लिए रिमाइंडर.

जब बॉल नीचे की दीवार से टकराती है, तो वह खेलने की सतह से गायब हो जाती है और बहुत ज़्यादा दिखती है. आप अगले चरण में फ़्लेम के इफ़ेक्ट की मदद से इस आर्टफ़ैक्ट को संभालते हैं.

अब जब आपके पास बॉल, गेम की दीवारों से टकरा रही है, तो यह पक्का है कि खिलाड़ी को बॉल को हिट करने के लिए बैट देना मददगार होगा...

7. बॉल पर बल्लेबाज़ी करें

बैट बनाएं

गेम के अंदर बॉल को खेल में रखने के लिए बैट जोड़ने के लिए,

  1. lib/src/config.dart फ़ाइल में कुछ कॉन्सटेंट नीचे दिए गए तरीके से डालें.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;                               // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;                               // To here.

batHeight और batWidth कॉन्सटेंट अपने-आप विस्तार से समझाए जा सकते हैं. वहीं दूसरी ओर, batStep कॉन्स्टेंट को दिखाने के लिए, एक्सप्लेनेशन की ज़रूरत होती है. इस गेम में बॉल से इंटरैक्ट करने के लिए, खिलाड़ी प्लैटफ़ॉर्म के हिसाब से माउस या उंगली से बैट को खींच सकता है या कीबोर्ड का इस्तेमाल कर सकता है. batStep कॉन्सटेंट यह कॉन्फ़िगर करता है कि लेफ़्ट या राइट ऐरो वाले हर बटन को दबाकर, बैट को कितनी दूर तक चलाया जा सकता है.

  1. Bat कॉम्पोनेंट क्लास को इस तरह परिभाषित करें.

lib/src/components/bat.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';

class Bat extends PositionComponent
    with DragCallbacks, HasGameReference<BrickBreaker> {
  Bat({
    required this.cornerRadius,
    required super.position,
    required super.size,
  }) : super(
          anchor: Anchor.center,
          children: [RectangleHitbox()],
        );

  final Radius cornerRadius;

  final _paint = Paint()
    ..color = const Color(0xff1e6091)
    ..style = PaintingStyle.fill;

  @override
  void render(Canvas canvas) {
    super.render(canvas);
    canvas.drawRRect(
        RRect.fromRectAndRadius(
          Offset.zero & size.toSize(),
          cornerRadius,
        ),
        _paint);
  }

  @override
  void onDragUpdate(DragUpdateEvent event) {
    super.onDragUpdate(event);
    position.x = (position.x + event.localDelta.x).clamp(0, game.width);
  }

  void moveBy(double dx) {
    add(MoveToEffect(
      Vector2((position.x + dx).clamp(0, game.width), position.y),
      EffectController(duration: 0.1),
    ));
  }
}

इस कॉम्पोनेंट में कुछ नई सुविधाएं जोड़ी गई हैं.

सबसे पहले, बैट कॉम्पोनेंट PositionComponent है. यह RectangleComponent या CircleComponent नहीं है. इसका मतलब है कि इस कोड को स्क्रीन पर Bat रेंडर करना होगा. ऐसा करने के लिए, यह render कॉलबैक को बदल देता है.

canvas.drawRRect (राउंड रेक्टैंगल बनाएं) कॉल को करीब से देखने पर, आपके मन में यह सवाल आ सकता है, "रेक्टैंगल कहां है?" Offset.zero & size.toSize(), dart:ui Offset क्लास पर operator & ओवरलोड का फ़ायदा उठाता है, जिससे Rects बनते हैं. इस शॉर्टहैंड की मदद से हो सकता है कि शुरुआत में आपको भ्रम हो जाए, लेकिन इसे बार-बार लोअर लेवल के फ़्लटर और फ़्लेम कोड में देखा जा सकता है.

दूसरा विकल्प, प्लैटफ़ॉर्म के हिसाब से इस Bat कॉम्पोनेंट को उंगली या माउस से खींचा जा सकता है. इस सुविधा को लागू करने के लिए, DragCallbacks मिक्सइन जोड़ें और onDragUpdate इवेंट को बदलें.

आखिर में, Bat कॉम्पोनेंट को कीबोर्ड कंट्रोल का जवाब देना होगा. moveBy फ़ंक्शन की मदद से अन्य कोड, इस बैट को वर्चुअल पिक्सल की एक तय संख्या तक बाईं या दाईं ओर जाने के लिए कह सकता है. यह फ़ंक्शन, फ़्लेम गेम इंजन की एक नई सुविधा उपलब्ध कराता है: Effect. इस कॉम्पोनेंट के चाइल्ड के तौर पर MoveToEffect ऑब्जेक्ट जोड़ने पर, प्लेयर को नई पोज़िशन में बैट मिलेगा. अलग-अलग तरह के इफ़ेक्ट इस्तेमाल करने के लिए, फ़्लेम में Effect का कलेक्शन उपलब्ध है.

इफ़ेक्ट के कंस्ट्रक्टर के आर्ग्युमेंट में, game गैटर का रेफ़रंस शामिल होता है. इसलिए, आपने इस क्लास में HasGameReference मिक्सिन को शामिल किया है. यह मिक्सिन, कॉम्पोनेंट ट्री के सबसे ऊपर मौजूद BrickBreaker इंस्टेंस को ऐक्सेस करने के लिए, इस कॉम्पोनेंट में एक तरह का सुरक्षित game ऐक्सेसर जोड़ता है.

  1. BrickBreaker को Bat उपलब्ध कराने के लिए, lib/src/components/components.dart फ़ाइल को नीचे बताए गए तरीके से अपडेट करें.

lib/src/components/components.dart

export 'ball.dart';
export 'bat.dart';                                              // Add this export
export 'play_area.dart';

दुनिया को बैट दिखाएं

Bat कॉम्पोनेंट को गेम की दुनिया में जोड़ने के लिए, BrickBreaker को नीचे बताए गए तरीके से अपडेट करें.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/events.dart';                             // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart';                         // And this import
import 'package:flutter/services.dart';                         // And this

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents {                // Modify this line
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    world.add(Bat(                                              // Add from here...
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95)));          // To here

    debugMode = true;
  }

  @override                                                     // Add from here...
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
    }
    return KeyEventResult.handled;
  }                                                             // To here
}

KeyboardEvents मिक्सिन और ओवरराइड किया गया onKeyEvent तरीका जोड़ने से, कीबोर्ड इनपुट हैंडल होता है. बैट को सही तरीके से मूव करने के लिए, पहले जोड़े गए कोड को याद रखें.

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

इस समय गेम खेलने पर, आपको पता चलता है कि बॉल को इंटरसेप्ट करने के लिए, बैट को हिला-डुलाया जा सकता है. हालांकि, आपको कोई जवाब नहीं दिखेगा. हालांकि, Ball के कोलिज़न डिटेक्शन कोड में आपने डीबग लॉग किया था.

इसे अभी ठीक करने का समय आ गया है. Ball कॉम्पोनेंट में नीचे बताए गए तरीके से बदलाव करें.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';                           // Add this import
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import 'bat.dart';                                             // And this import
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(                                       // Modify from here...
          delay: 0.35,
        ));
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x = velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else {                                                    // To here.
      debugPrint('collision with $other');
    }
  }
}

कोड में किए गए ये बदलाव, दो अलग-अलग समस्याओं को ठीक करते हैं.

सबसे पहले, यह स्क्रीन के निचले हिस्से से टच करते ही बॉल की मौजूदगी को ठीक कर देता है. इस समस्या को हल करने के लिए, आपको removeFromParent कॉल को RemoveEffect से बदलना होगा. जब बॉल देखने लायक जगह से बाहर निकल जाती है, तो RemoveEffect उसे गेम की दुनिया से हटा देता है.

दूसरा, इन बदलावों से बैट और बॉल के बीच टकराव की स्थिति ठीक हो जाती है. यह हैंडलिंग कोड खिलाड़ी के लिए काफ़ी फ़ायदेमंद है. जब तक खिलाड़ी बल्ले से बॉल को छूता रहेगा, तब तक बॉल स्क्रीन पर ऊपर वापस चली जाती है. अगर आपको लगता है कि आपको यह गेम नहीं लगता और आपको असली दिखने वाली चीज़ों को समझना है, तो इस तरीके में बदलाव करें, ताकि यह आपके गेम के हिसाब से सही हो.

velocity के अपडेट की जटिलता के बारे में बताना ज़रूरी है. यह सिर्फ़ वेलोसिटी के y कॉम्पोनेंट को रिवर्स नहीं करता, जैसा कि दीवार से होने वाले टकरावों के लिए किया गया था. यह x कॉम्पोनेंट को भी इस तरह अपडेट करता है, जो संपर्क के समय बल्ले और बॉल की स्थिति पर निर्भर करता है. इससे खिलाड़ी को बॉल पर ज़्यादा कंट्रोल मिलता है. हालांकि, यह जानकारी सिर्फ़ खेलने के अलावा, खिलाड़ी को किसी और तरीके से नहीं दी जाती.

अब जब आपके पास एक ऐसा बैट है जिससे बॉल को हिट करना है, तो अच्छी बात होगी कि बॉल के साथ कुछ ईंटों को तोड़ा जा सके!

8. विज्ञापन की बारीकियां

ईंटें बनाना

गेम में ब्रिक्स जोड़ने के लिए,

  1. lib/src/config.dart फ़ाइल में कुछ कॉन्सटेंट नीचे दिए गए तरीके से डालें.

lib/src/config.dart

import 'package:flutter/material.dart';                         // Add this import

const brickColors = [                                           // Add this const
  Color(0xfff94144),
  Color(0xfff3722c),
  Color(0xfff8961e),
  Color(0xfff9844a),
  Color(0xfff9c74f),
  Color(0xff90be6d),
  Color(0xff43aa8b),
  Color(0xff4d908e),
  Color(0xff277da1),
  Color(0xff577590),
];

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015;                          // Add from here...
final brickWidth =
    (gameWidth - (brickGutter * (brickColors.length + 1)))
    / brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03;                                // To here.
  1. Brick कॉम्पोनेंट को नीचे बताए गए तरीके से डालें.

lib/src/components/brick.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
      : super(
          size: Vector2(brickWidth, brickHeight),
          anchor: Anchor.center,
          paint: Paint()
            ..color = color
            ..style = PaintingStyle.fill,
          children: [RectangleHitbox()],
        );

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();

    if (game.world.children.query<Brick>().length == 1) {
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

अब तक, इस कोड के ज़्यादातर हिस्से से जुड़ी जानकारी मिल चुकी होगी. यह कोड, RectangleComponent का इस्तेमाल करता है. इसमें कॉम्पोनेंट ट्री के सबसे ऊपर, टक्कर का पता लगाने की सुविधा और BrickBreaker गेम का टाइप-सेफ़ रेफ़रंस होता है.

इस कोड की ओर से पेश किया जाने वाला सबसे अहम नया सिद्धांत यह है कि खिलाड़ी जीत की शर्त कैसे हासिल करता है. जीत की स्थिति की जांच के बाद, दुनिया भर से ईंटों के बारे में पता चलता है और यह पक्का हो जाता है कि सिर्फ़ एक ईंट बची है. यह थोड़ा भ्रामक हो सकता है, क्योंकि पिछली पंक्ति इस ईंट को अपने मूल स्थान से निकाल देती है.

मुख्य बात यह है कि कॉम्पोनेंट हटाने के लिए, सूची में एक निर्देश दिया जाता है. यह कोड इस कोड के चलने के बाद और गेम की अगली टिक से पहले ब्रिक को हटा देता है.

Brick कॉम्पोनेंट को BrickBreaker का ऐक्सेस देने के लिए, lib/src/components/components.dart में नीचे बताए गए तरीके से बदलाव करें.

lib/src/components/components.dart

export 'ball.dart';
export 'bat.dart';
export 'brick.dart';                                            // Add this export
export 'play_area.dart';

दुनिया को ईंटों से दिखाएं

Ball कॉम्पोनेंट को अपडेट करने के लिए, यहां दिया गया तरीका अपनाएं.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';                                            // Add this import
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
    required this.difficultyModifier,                           // Add this parameter
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);

  final Vector2 velocity;
  final double difficultyModifier;                              // Add this member

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(
          delay: 0.35,
        ));
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x = velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else if (other is Brick) {                                // Modify from here...
      if (position.y < other.position.y - other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.y > other.position.y + other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.x < other.position.x) {
        velocity.x = -velocity.x;
      } else if (position.x > other.position.x) {
        velocity.x = -velocity.x;
      }
      velocity.setFrom(velocity * difficultyModifier);          // To here.
    }
  }
}

इससे सिर्फ़ एक नए पहलू के बारे में पता चलता है. कठिनाई से जुड़ी कार्रवाई करने वाला ऐसा टूल जो ईंट से हर एक टकराने के बाद, बॉल की वेलोसिटी को बढ़ाता है. इस ट्यून किए जा सकने वाले पैरामीटर को टेस्ट करना ज़रूरी है, ताकि आपके गेम के हिसाब से सही कठिनाई का कर्व का पता लगाया जा सके.

BrickBreaker गेम में नीचे बताए गए तरीके से बदलाव करें.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents {
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(
        difficultyModifier: difficultyModifier,                 // Add this argument
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    world.add(Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95)));

    await world.addAll([                                        // Add from here...
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);                                                         // To here.

    debugMode = true;
  }

  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
    }
    return KeyEventResult.handled;
  }
}

अगर मौजूदा समय में गेम को पहले की तरह खेला जाता है, तो गेम की सभी मुख्य तकनीक दिखाई जाती हैं. आप डीबग करने की सुविधा को बंद कर सकते हैं और इसे 'हो गया' कह सकते हैं, लेकिन इसमें कुछ कमी लग रही है.

एक स्क्रीन शॉट, जिसमें बॉल, बैट, और खेलने की जगह पर मौजूद ज़्यादातर ईंटों के साथ ब्रिक_ब्रेकर दिखाया गया है. हर कॉम्पोनेंट में डीबग करने के लेबल मौजूद हैं

वेलकम स्क्रीन, ओवर स्क्रीन गेम, और स्कोर के बारे में आपका क्या ख्याल है? Flutter में इन सुविधाओं को गेम में जोड़ा जा सकता है. ऐसा करने पर, आपको इन सुविधाओं को बेहतर बनाने में मदद मिलेगी.

9. गेम जीतें

Play की स्थितियां जोड़ें

इस स्टेप में, आपको Flame गेम को Flutter रैपर में एम्बेड करना होगा. इसके बाद, वेलकम, गेम ओवर, और जीती गई स्क्रीन के लिए, Flutter ओवरले जोड़ना होगा.

सबसे पहले, गेम और उसके कॉम्पोनेंट की फ़ाइलों में बदलाव किया जाता है, ताकि उन्हें प्ले की स्थिति के तौर पर लागू किया जा सके. इससे यह पता चलेगा कि ओवरले दिखाना है या नहीं. अगर है, तो किसकी.

  1. BrickBreaker गेम में इस तरीके से बदलाव करें.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'components/components.dart';
import 'config.dart';

enum PlayState { welcome, playing, gameOver, won }              // Add this enumeration

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents, TapDetector {   // Modify this line
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  late PlayState _playState;                                    // Add from here...
  PlayState get playState => _playState;
  set playState(PlayState playState) {
    _playState = playState;
    switch (playState) {
      case PlayState.welcome:
      case PlayState.gameOver:
      case PlayState.won:
        overlays.add(playState.name);
      case PlayState.playing:
        overlays.remove(PlayState.welcome.name);
        overlays.remove(PlayState.gameOver.name);
        overlays.remove(PlayState.won.name);
    }
  }                                                             // To here.

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    playState = PlayState.welcome;                              // Add from here...
  }

  void startGame() {
    if (playState == PlayState.playing) return;

    world.removeAll(world.children.query<Ball>());
    world.removeAll(world.children.query<Bat>());
    world.removeAll(world.children.query<Brick>());

    playState = PlayState.playing;                              // To here.

    world.add(Ball(
        difficultyModifier: difficultyModifier,
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    world.add(Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95)));

    world.addAll([                                              // Drop the await
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);
  }                                                             // Drop the debugMode

  @override                                                     // Add from here...
  void onTap() {
    super.onTap();
    startGame();
  }                                                             // To here.

  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
      case LogicalKeyboardKey.space:                            // Add from here...
      case LogicalKeyboardKey.enter:
        startGame();                                            // To here.
    }
    return KeyEventResult.handled;
  }

  @override
  Color backgroundColor() => const Color(0xfff2e8cf);          // Add this override
}

यह कोड BrickBreaker गेम की एक बड़ी डील को बदल देता है. playState की गणना करने में बहुत मेहनत लगती है. इससे पता चलता है कि खिलाड़ी किस लेवल पर पहुंच रहा है, खेल रहा है, और गेम में हार या जीत हासिल कर रहा है. फ़ाइल के सबसे ऊपर, एन्युमरेशन तय किया जाता है. इसके बाद, उसे छिपी हुई स्थिति के तौर पर इंस्टैंशिएट करके, उससे मेल खाने वाले गैटर और सेटर की मदद ली जाती है. जब गेम के अलग-अलग हिस्से, गेम की स्थिति में बदलाव करते हैं, तब ये गैटर और सेटर ओवरले में बदलाव करने की सुविधा देते हैं.

इसके बाद, आपने onLoad में मौजूद कोड को onLoad और नए startGame तरीके में बांटा. इस बदलाव से पहले, गेम को रीस्टार्ट करके सिर्फ़ नया गेम शुरू किया जा सकता था. इन सुविधाओं के साथ, खिलाड़ी अब इन मुश्किल तरीकों के बिना भी नया गेम शुरू कर सकता है.

खिलाड़ी को नया गेम शुरू करने की अनुमति देने के लिए, आपने गेम के लिए दो नए हैंडलर कॉन्फ़िगर किए हैं. आपने एक टैप हैंडलर जोड़ा और कीबोर्ड हैंडलर को बढ़ाया है, ताकि उपयोगकर्ता कई मोडलिटी में नया गेम शुरू कर सके. खेलने की स्थिति के मॉडल के साथ, खिलाड़ी के जीतने या हारने पर, खेलने की स्थिति के ट्रांज़िशन को ट्रिगर करने के लिए कॉम्पोनेंट को अपडेट करना ज़रूरी होता है.

  1. Ball कॉम्पोनेंट में नीचे बताए गए तरीके से बदलाव करें.

lib/src/components/ball.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
    required this.difficultyModifier,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);

  final Vector2 velocity;
  final double difficultyModifier;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(
            delay: 0.35,
            onComplete: () {                                    // Modify from here
              game.playState = PlayState.gameOver;
            }));                                                // To here.
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x = velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else if (other is Brick) {
      if (position.y < other.position.y - other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.y > other.position.y + other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.x < other.position.x) {
        velocity.x = -velocity.x;
      } else if (position.x > other.position.x) {
        velocity.x = -velocity.x;
      }
      velocity.setFrom(velocity * difficultyModifier);
    }
  }
}

इस छोटे से बदलाव से RemoveEffect में onComplete कॉलबैक जुड़ जाता है, जो gameOver प्ले स्टेट को ट्रिगर करता है. यह सही लगता है अगर खिलाड़ी बॉल को स्क्रीन के नीचे तक जाने देता है.

  1. Brick कॉम्पोनेंट में नीचे बताए गए तरीके से बदलाव करें.

lib/src/components/brick.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
      : super(
          size: Vector2(brickWidth, brickHeight),
          anchor: Anchor.center,
          paint: Paint()
            ..color = color
            ..style = PaintingStyle.fill,
          children: [RectangleHitbox()],
        );

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();

    if (game.world.children.query<Brick>().length == 1) {
      game.playState = PlayState.won;                          // Add this line
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

वहीं दूसरी ओर, अगर खिलाड़ी सभी ईंटें तोड़ सकता है, तो इसका मतलब है कि उसने "गेम जीता" है स्क्रीन. बहुत अच्छे खिलाड़ी, बहुत अच्छे!

Flutter रैपर जोड़ें

गेम को एम्बेड करने और प्ले स्टेट ओवरले जोड़ने के लिए, Flutter शेल जोड़ें.

  1. lib/src में एक widgets डायरेक्ट्री बनाएं.
  2. कोई game_app.dart फ़ाइल जोड़ें और उस फ़ाइल में यह कॉन्टेंट शामिल करें.

lib/src/widgets/game_app.dart

import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

import '../brick_breaker.dart';
import '../config.dart';

class GameApp extends StatelessWidget {
  const GameApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color(0xffa9d6e5),
                Color(0xfff2e8cf),
              ],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: FittedBox(
                  child: SizedBox(
                    width: gameWidth,
                    height: gameHeight,
                    child: GameWidget.controlled(
                      gameFactory: BrickBreaker.new,
                      overlayBuilderMap: {
                        PlayState.welcome.name: (context, game) => Center(
                              child: Text(
                                'TAP TO PLAY',
                                style:
                                    Theme.of(context).textTheme.headlineLarge,
                              ),
                            ),
                        PlayState.gameOver.name: (context, game) => Center(
                              child: Text(
                                'G A M E   O V E R',
                                style:
                                    Theme.of(context).textTheme.headlineLarge,
                              ),
                            ),
                        PlayState.won.name: (context, game) => Center(
                              child: Text(
                                'Y O U   W O N ! ! !',
                                style:
                                    Theme.of(context).textTheme.headlineLarge,
                              ),
                            ),
                      },
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

इस फ़ाइल का ज़्यादातर कॉन्टेंट, Flutter विजेट ट्री बिल्ड के बाद मिलता है. फ़्लेम के खास पार्ट में, BrickBreaker गेम इंस्टेंस को बनाने और मैनेज करने के लिए, GameWidget.controlled का इस्तेमाल किया जाता है. साथ ही, GameWidget के लिए नया overlayBuilderMap आर्ग्युमेंट बनाया गया है.

इस overlayBuilderMap की कुंजियां उन ओवरले के साथ अलाइन होनी चाहिए जिन्हें BrickBreaker में playState सेटर ने जोड़ा या हटाया है. जो ओवरले इस मैप में नहीं है उसे सेट करने का प्रयास करने पर चारों ओर दुखी चेहरे दिखाई देते हैं.

  1. स्क्रीन पर यह नई सुविधा पाने के लिए, lib/main.dart फ़ाइल को इस कॉन्टेंट से बदलें.

lib/main.dart

import 'package:flutter/material.dart';

import 'src/widgets/game_app.dart';

void main() {
  runApp(const GameApp());
}

अगर इस कोड को iOS, Linux, Windows या वेब पर चलाया जाता है, तो गेम में इसके हिसाब से आउटपुट दिखता है. अगर macOS या Android को टारगेट किया जाता है, तो google_fonts को दिखाने के लिए आपको आखिरी बार एक ट्वीक करना होगा.

फ़ॉन्ट का ऐक्सेस चालू करना

Android के लिए इंटरनेट की अनुमति जोड़ें

Android के लिए, आपको इंटरनेट की अनुमति देनी होगी. अपने AndroidManifest.xml में दिए गए तरीके का इस्तेमाल करें.

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Add the following line -->
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:label="brick_breaker"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

macOS के लिए एनटाइटलमेंट फ़ाइलों में बदलाव करना

macOS के लिए, आपके पास बदलाव करने के लिए दो फ़ाइलें होती हैं.

  1. DebugProfile.entitlements फ़ाइल में बदलाव करें, ताकि वह नीचे दिए गए कोड से मेल खाए.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- to here. -->
</dict>
</plist>
  1. Release.entitlements फ़ाइल में बदलाव करें, ताकि वह नीचे दिए गए कोड से मेल खाए

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- to here. -->
</dict>
</plist>

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

10. स्कोर बनाए रखें

गेम में स्कोर जोड़ें

इस चरण में, Flutter के कॉन्टेक्स्ट के साथ गेम के स्कोर को सार्वजनिक किया जाता है. इस चरण में, आपको फ़्लेम गेम से लेकर, आस-पास के Flutter स्टेट मैनेजमेंट तक स्थिति के बारे में पता चलता है. इससे, जब भी खिलाड़ी ब्रिक तोड़ता है, तो गेम कोड स्कोर को अपडेट कर पाता है.

  1. BrickBreaker गेम में इस तरीके से बदलाव करें.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'components/components.dart';
import 'config.dart';

enum PlayState { welcome, playing, gameOver, won }

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents, TapDetector {
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final ValueNotifier<int> score = ValueNotifier(0);            // Add this line
  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  late PlayState _playState;
  PlayState get playState => _playState;
  set playState(PlayState playState) {
    _playState = playState;
    switch (playState) {
      case PlayState.welcome:
      case PlayState.gameOver:
      case PlayState.won:
        overlays.add(playState.name);
      case PlayState.playing:
        overlays.remove(PlayState.welcome.name);
        overlays.remove(PlayState.gameOver.name);
        overlays.remove(PlayState.won.name);
    }
  }

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    playState = PlayState.welcome;
  }

  void startGame() {
    if (playState == PlayState.playing) return;

    world.removeAll(world.children.query<Ball>());
    world.removeAll(world.children.query<Bat>());
    world.removeAll(world.children.query<Brick>());

    playState = PlayState.playing;
    score.value = 0;                                            // Add this line

    world.add(Ball(
        difficultyModifier: difficultyModifier,
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    world.add(Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95)));

    world.addAll([
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);
  }

  @override
  void onTap() {
    super.onTap();
    startGame();
  }

  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
      case LogicalKeyboardKey.space:
      case LogicalKeyboardKey.enter:
        startGame();
    }
    return KeyEventResult.handled;
  }

  @override
  Color backgroundColor() => const Color(0xfff2e8cf);
}

score को गेम में जोड़ने पर, गेम की स्थिति को Flutter के स्टेट मैनेजमेंट से जोड़ दिया जाता है.

  1. अगर कोई खिलाड़ी ब्रिक्स तोड़ता है, तो स्कोर में पॉइंट जोड़ने के लिए, Brick क्लास में बदलाव करें.

lib/src/components/brick.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
      : super(
          size: Vector2(brickWidth, brickHeight),
          anchor: Anchor.center,
          paint: Paint()
            ..color = color
            ..style = PaintingStyle.fill,
          children: [RectangleHitbox()],
        );

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();
    game.score.value++;                                         // Add this line

    if (game.world.children.query<Brick>().length == 1) {
      game.playState = PlayState.won;
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

एक आकर्षक गेम बनाएं

अब आप Flutter में स्कोर बनाए रख सकते हैं. इसलिए, अब समय आ गया है कि सभी विजेट एक साथ रखें, ताकि यह अच्छा दिखे.

  1. lib/src/widgets में score_card.dart बनाएं और इन्हें जोड़ें.

lib/src/widgets/score_card.dart

import 'package:flutter/material.dart';

class ScoreCard extends StatelessWidget {
  const ScoreCard({
    super.key,
    required this.score,
  });

  final ValueNotifier<int> score;

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<int>(
      valueListenable: score,
      builder: (context, score, child) {
        return Padding(
          padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
          child: Text(
            'Score: $score'.toUpperCase(),
            style: Theme.of(context).textTheme.titleLarge!,
          ),
        );
      },
    );
  }
}
  1. lib/src/widgets में overlay_screen.dart बनाएं और यह कोड जोड़ें.

इससे ओवरले स्क्रीन पर मूवमेंट और स्टाइल जोड़ने के लिए, flutter_animate पैकेज की पावर का इस्तेमाल करके ओवरले को ज़्यादा बेहतर बनाया जा सकता है.

lib/src/widgets/overlay_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';

class OverlayScreen extends StatelessWidget {
  const OverlayScreen({
    super.key,
    required this.title,
    required this.subtitle,
  });

  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: const Alignment(0, -0.15),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            title,
            style: Theme.of(context).textTheme.headlineLarge,
          ).animate().slideY(duration: 750.ms, begin: -3, end: 0),
          const SizedBox(height: 16),
          Text(
            subtitle,
            style: Theme.of(context).textTheme.headlineSmall,
          )
              .animate(onPlay: (controller) => controller.repeat())
              .fadeIn(duration: 1.seconds)
              .then()
              .fadeOut(duration: 1.seconds),
        ],
      ),
    );
  }
}

flutter_animate की सुविधाओं के बारे में ज़्यादा जानने के लिए, Flutter में अगली-पीढ़ी की टेक्नोलॉजी के यूज़र इंटरफ़ेस (यूआई) बनाना कोडलैब (कोड बनाना सीखना) देखें.

यह कोड GameApp कॉम्पोनेंट में काफ़ी बदल गया है. सबसे पहले, score को ऐक्सेस करने के लिए ScoreCard को चालू करने के लिए , आपको इसे StatelessWidget से StatefulWidget में बदलना होगा. स्कोर कार्ड जोड़ने के लिए गेम के ऊपर स्कोर को स्टैक करने के लिए Column जोड़ना ज़रूरी है.

दूसरा, वेलकम, गेम ओवर, और जीतने वाले अनुभवों को बेहतर बनाने के लिए, आपने नया OverlayScreen विजेट जोड़ा है.

lib/src/widgets/game_app.dart

import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart';                                   // Add this import
import 'score_card.dart';                                       // And this one too

class GameApp extends StatefulWidget {                          // Modify this line
  const GameApp({super.key});

  @override                                                     // Add from here...
  State<GameApp> createState() => _GameAppState();
}

class _GameAppState extends State<GameApp> {
  late final BrickBreaker game;

  @override
  void initState() {
    super.initState();
    game = BrickBreaker();
  }                                                             // To here.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color(0xffa9d6e5),
                Color(0xfff2e8cf),
              ],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: Column(                                  // Modify from here...
                  children: [
                    ScoreCard(score: game.score),
                    Expanded(
                      child: FittedBox(
                        child: SizedBox(
                          width: gameWidth,
                          height: gameHeight,
                          child: GameWidget(
                            game: game,
                            overlayBuilderMap: {
                              PlayState.welcome.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'TAP TO PLAY',
                                    subtitle: 'Use arrow keys or swipe',
                                  ),
                              PlayState.gameOver.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'G A M E   O V E R',
                                    subtitle: 'Tap to Play Again',
                                  ),
                              PlayState.won.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'Y O U   W O N ! ! !',
                                    subtitle: 'Tap to Play Again',
                                  ),
                            },
                          ),
                        ),
                      ),
                    ),
                  ],
                ),                                              // To here.
              ),
            ),
          ),
        ),
      ),
    );
  }
}

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

brick_breaker का स्क्रीन शॉट, जिसमें गेम से पहले की स्क्रीन दिखाई गई है और उपयोगकर्ता को गेम खेलने के लिए स्क्रीन पर टैप करने का न्योता दिया गया है

ब्रिक_ब्रेकर का स्क्रीन शॉट, जिसमें गेम को चमगादड़ और कुछ ईंटों के ऊपर, स्क्रीन पर दिखाया गया है

11. बधाई हो

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

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

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

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

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