1. परिचय
Flame, Flutter पर आधारित 2D गेम इंजन है. इस कोडलैब में, आपको 1970 के दशक के क्लासिक वीडियो गेम में से एक, स्टीव वोज़्नियाक के ब्रेकआउट से मिलता-जुलता गेम बनाना है. बैट, बॉल, और ईंटें बनाने के लिए, Flame के कॉम्पोनेंट का इस्तेमाल किया जाएगा. इस कोडलैब में, बैट की मूवमेंट को ऐनिमेट करने के लिए, Flame के इफ़ेक्ट का इस्तेमाल किया जाएगा. साथ ही, यह भी देखा जाएगा कि Flame को Flutter के स्टेट मैनेजमेंट सिस्टम के साथ कैसे इंटिग्रेट किया जाता है.
पूरा होने पर, आपका गेम इस ऐनिमेटेड GIF की तरह दिखना चाहिए. हालांकि, यह थोड़ा धीमा होगा.
आपको क्या सीखने को मिलेगा
- Flame के काम करने के तरीके के बारे में बुनियादी बातें जानें. इसके लिए,
GameWidget
से शुरुआत करें. - गेम लूप का इस्तेमाल कैसे करें.
- Flame के
Component
कैसे काम करते हैं. ये Flutter केWidget
s की तरह होते हैं. - टकरावों को कैसे मैनेज करें.
Component
को ऐनिमेट करने के लिएEffect
का इस्तेमाल कैसे करें.- किसी Flame गेम के ऊपर Flutter
Widget
s को कैसे ओवरले करें. - 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 के साथ काम करते हैं.
डेवलपमेंट का कोई टारगेट चुनें
Flutter, कई प्लैटफ़ॉर्म के लिए ऐप्लिकेशन बनाता है. आपका ऐप्लिकेशन इनमें से किसी भी ऑपरेटिंग सिस्टम पर काम कर सकता है:
- iOS
- Android
- Windows
- macOS
- Linux
- वेब
आम तौर पर, डेवलपमेंट के लिए एक ऑपरेटिंग सिस्टम को टारगेट के तौर पर चुना जाता है. यह वह ऑपरेटिंग सिस्टम है जिस पर आपका ऐप्लिकेशन डेवलपमेंट के दौरान चलता है.
उदाहरण के लिए: मान लें कि आपको Flutter ऐप्लिकेशन डेवलप करने के लिए, Windows लैपटॉप का इस्तेमाल करना है. इसके बाद, आपने Android को डेवलपमेंट के टारगेट के तौर पर चुना. अपने ऐप्लिकेशन की झलक देखने के लिए, यूएसबी केबल की मदद से Android डिवाइस को Windows लैपटॉप से कनेक्ट करें. इसके बाद, डेवलपमेंट मोड में मौजूद ऐप्लिकेशन को कनेक्ट किए गए Android डिवाइस या Android एम्युलेटर पर चलाएं. आपके पास डेवलपमेंट टारगेट के तौर पर Windows को चुनने का विकल्प होता है. इससे, डेवलपमेंट के दौरान आपका ऐप्लिकेशन, Windows ऐप्लिकेशन के तौर पर आपके एडिटर के साथ चलता है.
जारी रखने से पहले, अपनी पसंद का विकल्प चुनें. आपके पास बाद में, अपने ऐप्लिकेशन को दूसरे ऑपरेटिंग सिस्टम पर चलाने का विकल्प हमेशा होता है. डेवलपमेंट टारगेट चुनने से, अगला चरण आसानी से पूरा किया जा सकता है.
Flutter इंस्टॉल करना
Flutter SDK टूल इंस्टॉल करने के बारे में सबसे नए निर्देश, docs.flutter.dev पर देखे जा सकते हैं.
Flutter की वेबसाइट पर दिए गए निर्देशों में, SDK टूल, डेवलपमेंट टारगेट से जुड़े टूल, और एडिटर प्लगिन इंस्टॉल करने के बारे में बताया गया है. इस कोडलैब के लिए, यह सॉफ़्टवेयर इंस्टॉल करें:
- Flutter SDK
- Flutter प्लगिन के साथ Visual Studio Code
- चुने गए डेवलपमेंट टारगेट के लिए कंपाइलर सॉफ़्टवेयर. (Windows को टारगेट करने के लिए, आपको Visual Studio और macOS या iOS को टारगेट करने के लिए, Xcode की ज़रूरत होगी)
अगले सेक्शन में, अपना पहला Flutter प्रोजेक्ट बनाया जा सकता है.
अगर आपको किसी समस्या को हल करना है, तो आपको StackOverflow पर मौजूद इन सवालों और जवाबों से मदद मिल सकती है.
अक्सर पूछे जाने वाले सवाल
- मुझे Flutter SDK का पाथ कैसे मिलेगा?
- अगर Flutter कमांड नहीं मिलती है, तो मुझे क्या करना चाहिए?
- मैं "Waiting for another flutter command to release the startup lock" समस्या को कैसे ठीक करूं?
- मैं Flutter को यह कैसे बताऊं कि मेरा Android SDK कहां इंस्टॉल है?
flutter doctor --android-licenses
चलाते समय, Java से जुड़ी गड़बड़ी को कैसे ठीक किया जा सकता है?- मुझे Android
sdkmanager
टूल नहीं मिल रहा है. मैं इस समस्या को कैसे ठीक करूं? - मैं "
cmdline-tools
कॉम्पोनेंट मौजूद नहीं है" गड़बड़ी को कैसे ठीक करूं? - मैं Apple Silicon (M1) पर CocoaPods कैसे चलाऊं?
- मैं VS Code में सेव करने पर अपने-आप फ़ॉर्मैट होने की सुविधा को कैसे बंद करूं?
3. प्रोजेक्ट बनाना
अपना पहला Flutter प्रोजेक्ट बनाना
इसके लिए, VS Code खोलें और अपनी पसंद की डायरेक्ट्री में Flutter ऐप्लिकेशन टेंप्लेट बनाएं.
- Visual Studio Code लॉन्च करें.
- कमांड पैलेट (
F1
याCtrl+Shift+P
याShift+Cmd+P
) खोलें. इसके बाद, "flutter new" टाइप करें. जब यह दिखे, तब Flutter: New Project कमांड चुनें.
- ऐप्लिकेशन खाली करें को चुनें. वह डायरेक्ट्री चुनें जिसमें आपको प्रोजेक्ट बनाना है. यह ऐसी डायरेक्ट्री होनी चाहिए जिसके पाथ में कोई स्पेस न हो और जिसके लिए ज़्यादा अनुमतियों की ज़रूरत न हो. उदाहरण के लिए, आपकी होम डायरेक्ट्री या
C:\src\
.
- अपने प्रोजेक्ट को
brick_breaker
नाम दें. इस कोडलैब के बाकी हिस्से में, यह मान लिया गया है कि आपने अपने ऐप्लिकेशन का नाम रखा है.brick_breaker
अब Flutter, आपका प्रोजेक्ट फ़ोल्डर बनाता है और VS Code उसे खोलता है. अब आपको ऐप्लिकेशन के बेसिक स्ट्रक्चर के साथ, दो फ़ाइलों के कॉन्टेंट को बदलना होगा.
ऐप्लिकेशन को कॉपी करके चिपकाना
इससे, इस कोडलैब में दिया गया उदाहरण कोड आपके ऐप्लिकेशन में जुड़ जाता है.
- VS Code के बाएं पैनल में, Explorer पर क्लिक करें और
pubspec.yaml
फ़ाइल खोलें.
- इस फ़ाइल के कॉन्टेंट की जगह यह कॉन्टेंट डालें:
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
फ़ाइल में आपके ऐप्लिकेशन के बारे में बुनियादी जानकारी होती है. जैसे, उसका मौजूदा वर्शन, उसकी डिपेंडेंसी, और वे ऐसेट जिनके साथ उसे शिप किया जाएगा.
lib/
डायरेक्ट्री में मौजूदmain.dart
फ़ाइल खोलें.
- इस फ़ाइल के कॉन्टेंट की जगह यह कॉन्टेंट डालें:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
final game = FlameGame();
runApp(GameWidget(game: game));
}
- यह कोड चलाकर देखें कि सब कुछ ठीक से काम कर रहा है या नहीं. इसमें सिर्फ़ खाली काले बैकग्राउंड वाली नई विंडो दिखनी चाहिए. दुनिया का सबसे खराब वीडियो गेम अब 60fps पर रेंडर हो रहा है!
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
कॉम्पोनेंट होना चाहिए.
lib/src/components
नाम की नई डायरेक्ट्री में,play_area.dart
नाम की फ़ाइल बनाएं.- इस फ़ाइल में यह जानकारी जोड़ें.
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 के कॉम्पोनेंट को गेम की तकनीकों को दिखाने के लिए ऑप्टिमाइज़ किया गया है. यह कोडलैब, गेम लूप से शुरू होगा. इसके बारे में अगले चरण में बताया गया है.
- फ़ाइलें व्यवस्थित रखने के लिए, इस प्रोजेक्ट में मौजूद सभी कॉम्पोनेंट वाली फ़ाइल जोड़ें.
components.dart
मेंcomponents.dart
फ़ाइल बनाएं और इसमें यह कॉन्टेंट जोड़ें.lib/src/components
lib/src/components/components.dart
export 'play_area.dart';
export
डायरेक्टिव, import
डायरेक्टिव के उलट काम करता है. यह कुकी यह तय करती है कि किसी दूसरी फ़ाइल में इंपोर्ट किए जाने पर, यह फ़ाइल कौनसी सुविधाएं उपलब्ध कराएगी. नीचे दिए गए चरणों में नए कॉम्पोनेंट जोड़ने पर, इस फ़ाइल में ज़्यादा एंट्री जुड़ जाएंगी.
Flame गेम बनाना
पिछले चरण में बनाए गए लाल रंग के स्क्विगल को हटाने के लिए, Flame के लिए नई सबक्लास बनाएं FlameGame
.
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
ओवरराइड किए गए तरीके में, आपका कोड दो कार्रवाइयां करता है.
- यह विकल्प, व्यूफ़ाइंडर के लिए सबसे ऊपर बाईं ओर मौजूद जगह को ऐंकर के तौर पर कॉन्फ़िगर करता है. डिफ़ॉल्ट रूप से,
viewfinder
,(0,0)
के लिए एंकर के तौर पर, जगह के बीच का हिस्सा इस्तेमाल करता है. PlayArea
कोworld
में जोड़ता है. दुनिया, गेम की दुनिया को दिखाती है. यह अपने सभी चाइल्ड कोCameraComponent
s व्यू ट्रांसफ़ॉर्मेशन के ज़रिए प्रोजेक्ट करता है.
स्क्रीन पर गेम देखना
इस चरण में किए गए सभी बदलाव देखने के लिए, अपनी 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));
}
ये बदलाव करने के बाद, गेम को रीस्टार्ट करें. गेम, इस इमेज में दिखाए गए गेम जैसा होना चाहिए.
अगले चरण में, दुनिया में एक गेंद जोड़ी जाएगी और उसे घुमाया जाएगा!
5. गेंद को दिखाना
बॉल कॉम्पोनेंट बनाना
स्क्रीन पर घूमती हुई गेंद को दिखाने के लिए, एक और कॉम्पोनेंट बनाना होगा. इसके बाद, उसे गेम की दुनिया में जोड़ना होगा.
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
में बदलाव किया जा सकता है. इससे यह पता चलता है कि गेम के लुक और स्टाइल में क्या बदलाव हुए हैं.
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
अपडेट पर खास ध्यान दें. इस तरह, समय के साथ मोशन के अलग-अलग सिम्युलेशन को अपडेट किया जाता है.
- कॉम्पोनेंट की सूची में
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);
}
}
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. बल्ले से गेंद को मारना
बैट बनाना
गेम में गेंद को खेलने के लिए बैट जोड़ने के लिए,
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
कॉन्स्टेंट से यह तय होता है कि हर बार लेफ़्ट या राइट ऐरो की दबाने पर बैट कितना आगे बढ़ेगा.
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 &
ओवरलोड का फ़ायदा उठाता है. इससे Rect
s बनते हैं. शुरुआत में, आपको यह शॉर्टहैंड समझ में नहीं आ सकता. हालांकि, आपको यह Flutter और Flame के निचले लेवल के कोड में अक्सर दिखेगा.
दूसरा, इस Bat
कॉम्पोनेंट को प्लैटफ़ॉर्म के हिसाब से, उंगली या माउस का इस्तेमाल करके ड्रैग किया जा सकता है. इस सुविधा को लागू करने के लिए, DragCallbacks
मिक्सइन जोड़ें और onDragUpdate
इवेंट को बदलें.
आखिर में, Bat
कॉम्पोनेंट को कीबोर्ड कंट्रोल का जवाब देना होगा. moveBy
फ़ंक्शन की मदद से, अन्य कोड इस बैट को कुछ वर्चुअल पिक्सल तक बाईं या दाईं ओर ले जाने के लिए कह सकते हैं. इस फ़ंक्शन से, Flame गेम इंजन की एक नई सुविधा मिलती है: Effect
s. MoveToEffect
ऑब्जेक्ट को इस कॉम्पोनेंट के चाइल्ड कॉम्पोनेंट के तौर पर जोड़ने पर, प्लेयर को बैट नई पोज़िशन में ऐनिमेशन के साथ दिखता है. Flame में कई तरह के इफ़ेक्ट लागू करने के लिए, Effect
का एक कलेक्शन उपलब्ध है.
Effect के कंस्ट्रक्टर आर्ग्युमेंट में, game
गेटर का रेफ़रंस शामिल होता है. इसलिए, इस क्लास में HasGameReference
मिक्सइन शामिल किया जाता है. यह मिक्सइन, इस कॉम्पोनेंट में टाइप-सेफ़ game
ऐक्सेसर जोड़ता है. इससे कॉम्पोनेंट ट्री के टॉप पर मौजूद BrickBreaker
इंस्टेंस को ऐक्सेस किया जा सकता है.
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. दीवार तोड़ना
ब्रिक बनाना
गेम में ब्रिक जोड़ने के लिए,
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.
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. गेम जीतना
गेम खेलने की स्थितियां जोड़ना
इस चरण में, आपको फ़्लेम गेम को फ़्लटर रैपर में एम्बेड करना होगा. इसके बाद, वेलकम, गेम ओवर, और जीत वाली स्क्रीन के लिए फ़्लटर ओवरले जोड़ने होंगे.
सबसे पहले, गेम और कॉम्पोनेंट फ़ाइलों में बदलाव करें, ताकि यह पता चल सके कि ओवरले दिखाना है या नहीं. अगर दिखाना है, तो कौन सा ओवरले दिखाना है.
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
तरीके में बांटा जाता है. इस बदलाव से पहले, गेम को रीस्टार्ट करके ही नया गेम शुरू किया जा सकता था. इन नई सुविधाओं की मदद से, अब खिलाड़ी को नया गेम शुरू करने के लिए, इस तरह के मुश्किल तरीके अपनाने की ज़रूरत नहीं पड़ेगी.
आपने खिलाड़ी को नया गेम शुरू करने की अनुमति देने के लिए, गेम के लिए दो नए हैंडलर कॉन्फ़िगर किए हैं. आपने टैप हैंडलर जोड़ा है और कीबोर्ड हैंडलर को बढ़ाया है, ताकि उपयोगकर्ता अलग-अलग मोड में नया गेम शुरू कर सके. प्ले स्टेट को मॉडल करने के बाद, कॉम्पोनेंट को अपडेट करना सही होगा. इससे खिलाड़ी के जीतने या हारने पर, प्ले स्टेट ट्रांज़िशन ट्रिगर हो जाएंगे.
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
प्ले स्टेट ट्रिगर हो जाती है. अगर खिलाड़ी गेंद को स्क्रीन के सबसे नीचे से बाहर जाने देता है, तो यह स्कोर सही होना चाहिए.
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 शेल जोड़ें.
lib/src
डायरेक्ट्री के नीचेwidgets
डायरेक्ट्री बनाएं.- एक
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
सेटर ने जोड़ा या हटाया है. इस मैप में मौजूद न होने वाले ओवरले को सेट करने की कोशिश करने पर, हर जगह नाखुश चेहरे दिखते हैं.
- स्क्रीन पर यह नई सुविधा पाने के लिए,
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 के लिए, आपके पास बदलाव करने के लिए दो फ़ाइलें हैं.
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>
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 स्टेट मैनेजमेंट में दिखाया जाता है. इससे गेम कोड, खिलाड़ी के हर ईंट को तोड़ने पर स्कोर को अपडेट कर पाता है.
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 के स्टेट मैनेजमेंट से जोड़ दिया है.
- जब कोई खिलाड़ी ईंटें तोड़ता है, तब स्कोर में पॉइंट जोड़ने के लिए,
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 में स्कोर सेव किया जा सकता है. इसलिए, अब समय है कि स्कोर को बेहतर तरीके से दिखाने के लिए विजेट बनाए जाएं.
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!,
),
);
},
);
}
}
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 पैकेज का इस्तेमाल किया है.
आगे क्या करना है?
यहां दिए गए कुछ कोडलैब देखें...
- Flutter में नई जनरेशन वाले यूज़र इंटरफ़ेस (यूआई) बनाना
- अपने Flutter ऐप्लिकेशन को बेहतर बनाएं
- अपने Flutter ऐप्लिकेशन में इन-ऐप्लिकेशन खरीदारी की सुविधा जोड़ना