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

1. परिचय

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

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

गेम खेलते समय की स्क्रीन रिकॉर्डिंग. गेम की स्पीड काफ़ी बढ़ गई है.

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

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

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

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

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

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

संपादक

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

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

VS Code में कुछ Flutter कोड

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

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

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

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

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

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

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

Flutter इंस्टॉल करना

Flutter SDK टूल इंस्टॉल करने के बारे में सबसे नए निर्देश, docs.flutter.dev पर देखे जा सकते हैं.

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

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

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

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

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

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

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

इसके लिए, VS Code खोलें और अपनी पसंद की डायरेक्ट्री में Flutter ऐप्लिकेशन टेंप्लेट बनाएं.

  1. Visual Studio Code लॉन्च करें.
  2. कमांड पैलेट (F1 या Ctrl+Shift+P या Shift+Cmd+P) खोलें. इसके बाद, "flutter new" टाइप करें. जब यह दिखे, तब Flutter: New Project कमांड चुनें.

VS Code with

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

नए ऐप्लिकेशन फ़्लो के हिस्से के तौर पर, VS Code में खाली ऐप्लिकेशन को चुना गया है

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

VS Code with

अब Flutter, आपका प्रोजेक्ट फ़ोल्डर बनाता है और VS Code उसे खोलता है. अब आपको ऐप्लिकेशन के बेसिक स्ट्रक्चर के साथ, दो फ़ाइलों के कॉन्टेंट को बदलना होगा.

ऐप्लिकेशन को कॉपी करके चिपकाना

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

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

VS Code का स्क्रीनशॉट. इसमें pubspec.yaml फ़ाइल की जगह को हाइलाइट करने वाले ऐरो दिखाए गए हैं

  1. इस फ़ाइल के कॉन्टेंट की जगह यह कॉन्टेंट डालें:

pubspec.yaml

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

environment:
  sdk: ^3.8.0

dependencies:
  flutter:
    sdk: flutter
  flame: ^1.28.1
  flutter_animate: ^4.5.2
  google_fonts: ^6.2.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true

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

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

VS Code का स्क्रीनशॉट. इसमें ऐरो से 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 पर रेंडर हो रहा है!

स्क्रीनशॉट में, brick_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 में जहां Widget है वहां Flame में Component है. Flutter ऐप्लिकेशन में विजेट के ट्री बनाए जाते हैं, जबकि Flame गेम में कॉम्पोनेंट के ट्री बनाए जाते हैं.

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

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

  1. फ़ाइलें व्यवस्थित रखने के लिए, इस प्रोजेक्ट में मौजूद सभी कॉम्पोनेंट वाली फ़ाइल जोड़ें. components.dart में components.dart फ़ाइल बनाएं और इसमें यह कॉन्टेंट जोड़ें.lib/src/components

lib/src/components/components.dart

export 'play_area.dart';

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

Flame गेम बनाना

पिछले चरण में बनाए गए लाल रंग के स्क्विगल को हटाने के लिए, 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. यह विकल्प, व्यूफ़ाइंडर के लिए सबसे ऊपर बाईं ओर मौजूद जगह को ऐंकर के तौर पर कॉन्फ़िगर करता है. डिफ़ॉल्ट रूप से, viewfinder, (0,0) के लिए एंकर के तौर पर, जगह के बीच का हिस्सा इस्तेमाल करता है.
  2. PlayArea को world में जोड़ता है. दुनिया, गेम की दुनिया को दिखाती है. यह अपने सभी चाइल्ड को CameraComponents व्यू ट्रांसफ़ॉर्मेशन के ज़रिए प्रोजेक्ट करता है.

स्क्रीन पर गेम देखना

इस चरण में किए गए सभी बदलाव देखने के लिए, अपनी 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;
  }
}

आपने पहले PlayArea को RectangleComponent का इस्तेमाल करके तय किया था. इसलिए, यह माना जा सकता है कि ज़्यादा शेप मौजूद हैं. CircleComponent, RectangleComponent से मिलता-जुलता है. इसलिए, 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 तक बढ़ाया जाता है.

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

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

अब गेम चलाने पर, वह इस तरह दिखना चाहिए.

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

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);
  }
}

RectangleHitbox कॉम्पोनेंट को RectangleComponent के चाइल्ड कॉम्पोनेंट के तौर पर जोड़ने से, टक्कर का पता लगाने के लिए एक हिट बॉक्स बन जाएगा. इसका साइज़ पैरंट कॉम्पोनेंट के साइज़ के बराबर होगा. 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 शर्त भी जोड़ी जाती है, ताकि गेंद के बल्ले के अलावा किसी और चीज़ से टकराने पर उसे हैंडल किया जा सके. अगर आपको लगता है, तो बाकी लॉजिक लागू करने के लिए एक छोटा सा रिमाइंडर.

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

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

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 बनते हैं. शुरुआत में, आपको यह शॉर्टहैंड समझ में नहीं आ सकता. हालांकि, आपको यह Flutter और Flame के निचले लेवल के कोड में अक्सर दिखेगा.

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

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

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

  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';

Add the bat to the world

गेम वर्ल्ड में 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(                                                  // Add from here...
      Bat(
        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(delay: 0.35));                         // Modify from 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 {                                                    // 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;
  }
}

गेम चलाने पर, आपको गेम के सभी मुख्य मेकेनिक दिखते हैं. आपके पास डीबग करने की सुविधा बंद करने का विकल्प है. हालांकि, ऐसा करने से कुछ ज़रूरी जानकारी नहीं मिल पाएगी.

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

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

9. गेम जीतना

गेम खेलने की स्थितियां जोड़ना

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

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

  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

impimport '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(
        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,
                          ),
                        ),
                      },
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

इस फ़ाइल में मौजूद ज़्यादातर कॉन्टेंट, स्टैंडर्ड फ़्लटर विजेट ट्री बिल्ड के मुताबिक है. Flame से जुड़े खास हिस्सों में, 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. स्कोर बनाए रखें

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

इस चरण में, गेम के स्कोर को आस-पास के फ़्लटर कॉन्टेक्स्ट में दिखाया जाता है. इस चरण में, Flame गेम से स्टेट को आस-पास के 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 कॉम्पोनेंट में काफ़ी बदलाव किया गया है. सबसे पहले, ScoreCard को score का ऐक्सेस देने के लिए , इसे 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(
        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 के छह टारगेट प्लैटफ़ॉर्म में से किसी पर भी चलाने में कोई समस्या नहीं आएगी. गेम कुछ इस तरह का होना चाहिए.

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

ब्रिक ब्रेकर गेम का स्क्रीनशॉट. इसमें गेम ओवर वाली स्क्रीन, बैट और कुछ ईंटों के ऊपर दिख रही है

11. बधाई हो

बधाई हो, आपने Flutter और Flame का इस्तेमाल करके गेम बना लिया है!

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

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

यहां दिए गए कुछ कोडलैब देखें...

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