ফ্লাটার সহ শিখার পরিচিতি

১. ভূমিকা

ফ্লেম হলো একটি ফ্লাটার-ভিত্তিক ২ডি গেম ইঞ্জিন। এই কোডল্যাবে, আপনি ৭০-এর দশকের অন্যতম ক্লাসিক ভিডিও গেম, স্টিভ ওজনিয়াকের ' ব্রেকআউট ' দ্বারা অনুপ্রাণিত হয়ে একটি গেম তৈরি করবেন। আপনি ব্যাট, বল এবং ইট আঁকার জন্য ফ্লেমের কম্পোনেন্ট ব্যবহার করবেন। আপনি ব্যাটের নড়াচড়া অ্যানিমেট করতে ফ্লেমের এফেক্ট ব্যবহার করবেন এবং দেখবেন কীভাবে ফ্লাটারের স্টেট ম্যানেজমেন্ট সিস্টেমের সাথে ফ্লেমকে ইন্টিগ্রেট করতে হয়।

সম্পূর্ণ হলে, আপনার গেমটি এই অ্যানিমেটেড জিআইএফটির মতো দেখতে হবে, তবে কিছুটা ধীরগতির।

একটি গেম খেলার স্ক্রিন রেকর্ডিং। গেমটির গতি উল্লেখযোগ্যভাবে বাড়িয়ে দেওয়া হয়েছে।

আপনি যা শিখবেন

  • ফ্লেম-এর মূল বিষয়গুলো কীভাবে কাজ করে, GameWidget দিয়ে শুরু করে।
  • গেম লুপ কীভাবে ব্যবহার করবেন।
  • ফ্লেমের Component কীভাবে কাজ করে। এগুলো ফ্লাটারের Widget অনুরূপ।
  • সংঘর্ষ কীভাবে সামাল দেবেন।
  • Component অ্যানিমেট করতে কীভাবে Effect ব্যবহার করবেন।
  • ফ্লেম গেমের উপরে কীভাবে ফ্লাটার Widget ওভারলে করবেন।
  • ফ্লাটারের স্টেট ম্যানেজমেন্টের সাথে ফ্লেমকে কীভাবে সংযুক্ত করবেন।

আপনি যা তৈরি করবেন

এই কোডল্যাবে, আপনি ফ্লাটার এবং ফ্লেম ব্যবহার করে একটি ২ডি গেম তৈরি করবেন। তৈরি হয়ে গেলে, আপনার গেমটিকে নিম্নলিখিত শর্তগুলো পূরণ করতে হবে:

  • ফ্লাটার সমর্থিত ছয়টি প্ল্যাটফর্মেই কাজ করে: অ্যান্ড্রয়েড, আইওএস, লিনাক্স, ম্যাকওএস, উইন্ডোজ এবং ওয়েব।
  • ফ্লেমের গেম লুপ ব্যবহার করে কমপক্ষে ৬০ এফপিএস বজায় রাখুন।
  • ৮০-এর দশকের আর্কেড গেমিংয়ের আবহ পুনরায় তৈরি করতে ফ্লাটারের google_fonts প্যাকেজ এবং flutter_animate এর মতো ফিচারগুলো ব্যবহার করুন।

২. আপনার ফ্লাটার পরিবেশ সেট আপ করুন।

সম্পাদক

এই কোডল্যাবটিকে সহজ করার জন্য, ধরে নেওয়া হচ্ছে যে ভিজ্যুয়াল স্টুডিও কোড (VS Code) আপনার ডেভেলপমেন্ট এনভায়রনমেন্ট। VS Code বিনামূল্যে পাওয়া যায় এবং এটি সমস্ত প্রধান প্ল্যাটফর্মে কাজ করে। আমরা এই কোডল্যাবের জন্য VS Code ব্যবহার করি কারণ এর নির্দেশাবলীতে ডিফল্টভাবে VS Code-এর নির্দিষ্ট শর্টকাটগুলো দেখানো হয়। এতে কাজগুলো আরও সহজবোধ্য হয়ে যায়: যেমন, "আপনার এডিটরে উপযুক্ত কাজটি করার জন্য X-টি করুন" এর পরিবর্তে বলা হয়, "X করার জন্য এই বাটনটিতে ক্লিক করুন" বা "এই কী-টি চাপুন"।

আপনি আপনার পছন্দের যেকোনো এডিটর ব্যবহার করতে পারেন: অ্যান্ড্রয়েড স্টুডিও, অন্যান্য ইন্টেলিজ আইডিই, ইম্যাক্স, ভিম, বা নোটপ্যাড++। এগুলো সবই ফ্লাটারের সাথে কাজ করে।

কিছু ফ্লাটার কোড সহ ভিএস কোড

একটি উন্নয়ন লক্ষ্য নির্বাচন করুন

ফ্লাটার একাধিক প্ল্যাটফর্মের জন্য অ্যাপ তৈরি করে। আপনার অ্যাপটি নিম্নলিখিত যেকোনো অপারেটিং সিস্টেমে চলতে পারে:

  • আইওএস
  • অ্যান্ড্রয়েড
  • উইন্ডোজ
  • ম্যাকওএস
  • লিনাক্স
  • ওয়েব

ডেভেলপমেন্টের লক্ষ্য হিসেবে একটি অপারেটিং সিস্টেম বেছে নেওয়া একটি প্রচলিত রীতি। ডেভেলপমেন্ট চলাকালীন আপনার অ্যাপটি এই অপারেটিং সিস্টেমেই চলে।

একটি ল্যাপটপ এবং তারের মাধ্যমে ল্যাপটপের সাথে সংযুক্ত একটি ফোনের চিত্র। ল্যাপটপটিকে হিসেবে চিহ্নিত করা হয়েছে

উদাহরণস্বরূপ: ধরুন আপনি আপনার ফ্লাটার অ্যাপটি তৈরি করার জন্য একটি উইন্ডোজ ল্যাপটপ ব্যবহার করছেন। এরপর আপনি আপনার ডেভেলপমেন্ট টার্গেট হিসেবে অ্যান্ড্রয়েড বেছে নিলেন। আপনার অ্যাপটি প্রিভিউ করার জন্য, আপনি একটি ইউএসবি ক্যাবলের মাধ্যমে আপনার উইন্ডোজ ল্যাপটপের সাথে একটি অ্যান্ড্রয়েড ডিভাইস সংযুক্ত করেন এবং আপনার নির্মাণাধীন অ্যাপটি সেই সংযুক্ত অ্যান্ড্রয়েড ডিভাইসে বা একটি অ্যান্ড্রয়েড এমুলেটরে চলে। আপনি ডেভেলপমেন্ট টার্গেট হিসেবে উইন্ডোজও বেছে নিতে পারতেন, যা আপনার এডিটরের পাশাপাশি আপনার নির্মাণাধীন অ্যাপটিকে একটি উইন্ডোজ অ্যাপ হিসেবে চালাত।

এগিয়ে যাওয়ার আগে আপনার পছন্দটি স্থির করুন। আপনি পরবর্তীতে যেকোনো সময় আপনার অ্যাপটি অন্যান্য অপারেটিং সিস্টেমে চালাতে পারবেন। উন্নয়নের লক্ষ্য নির্ধারণ করলে পরবর্তী পদক্ষেপটি আরও সহজ হয়।

ফ্লাটার ইনস্টল করুন

ফ্লাটার এসডিকে ইনস্টল করার সবচেয়ে হালনাগাদ নির্দেশাবলী docs.flutter.dev- এ পাওয়া যাবে।

ফ্লাটার ওয়েবসাইটের নির্দেশাবলীতে এসডিকে, ডেভেলপমেন্ট টার্গেট-সম্পর্কিত টুলস এবং এডিটর প্লাগইন ইনস্টল করার পদ্ধতি বর্ণনা করা হয়েছে। এই কোডল্যাবের জন্য, নিম্নলিখিত সফ্টওয়্যারগুলো ইনস্টল করুন:

  1. ফ্লাটার এসডিকে
  2. ফ্লাটার প্লাগইন সহ ভিজ্যুয়াল স্টুডিও কোড
  3. আপনার নির্বাচিত ডেভেলপমেন্ট টার্গেটের জন্য কম্পাইলার সফটওয়্যার। (উইন্ডোজের জন্য ভিজ্যুয়াল স্টুডিও অথবা ম্যাকওএস বা আইওএস-এর জন্য এক্সকোড প্রয়োজন)

পরবর্তী অংশে আপনি আপনার প্রথম ফ্লাটার প্রজেক্ট তৈরি করবেন।

যদি আপনার কোনো সমস্যা সমাধান করার প্রয়োজন হয়, তাহলে সমস্যা সমাধানের জন্য স্ট্যাকওভারফ্লো-এর এই প্রশ্ন ও উত্তরগুলো আপনার কাজে লাগতে পারে।

প্রায়শই জিজ্ঞাসিত প্রশ্নাবলী

৩. একটি প্রকল্প তৈরি করুন

আপনার প্রথম ফ্লাটার প্রজেক্ট তৈরি করুন

এর জন্য আপনাকে ভিএস কোড খুলে আপনার পছন্দের একটি ডিরেক্টরিতে ফ্লাটার অ্যাপ টেমপ্লেট তৈরি করতে হবে।

  1. ভিজ্যুয়াল স্টুডিও কোড চালু করুন।
  2. কমান্ড প্যালেট খুলুন ( F1 অথবা Ctrl+Shift+P অথবা Shift+Cmd+P ) তারপর "flutter new" টাইপ করুন। এটি প্রদর্শিত হলে, Flutter: New Project কমান্ডটি নির্বাচন করুন।

ভিএস কোড সহ

  1. খালি অ্যাপ্লিকেশন নির্বাচন করুন। আপনার প্রজেক্ট তৈরি করার জন্য একটি ডিরেক্টরি বেছে নিন। এটি এমন যেকোনো ডিরেক্টরি হতে পারে যার জন্য বিশেষ অনুমতির প্রয়োজন হয় না অথবা যার পাথে কোনো স্পেস নেই। উদাহরণস্বরূপ, আপনার হোম ডিরেক্টরি বা C:\src\

নতুন অ্যাপ্লিকেশন ফ্লো-এর অংশ হিসেবে খালি অ্যাপ্লিকেশনসহ ভিএস কোড নির্বাচিত হিসাবে দেখানো হচ্ছে।

  1. আপনার প্রজেক্টের নাম brick_breaker দিন। এই কোডল্যাবের বাকি অংশটি ধরে নিচ্ছে যে আপনি আপনার অ্যাপের নামও brick_breaker রেখেছেন।

ভিএস কোড সহ

ফ্লাটার এখন আপনার প্রজেক্ট ফোল্ডার তৈরি করবে এবং ভিএস কোড সেটি খুলবে। এখন আপনি অ্যাপটির একটি প্রাথমিক কাঠামো দিয়ে দুটি ফাইলের বিষয়বস্তু ওভাররাইট করবেন।

প্রাথমিক অ্যাপটি কপি ও পেস্ট করুন

এটি এই কোডল্যাবে দেওয়া উদাহরণ কোডটি আপনার অ্যাপে যুক্ত করে।

  1. VS Code-এর বাম প্যানে, Explorer-এ ক্লিক করুন এবং pubspec.yaml ফাইলটি খুলুন।

VS কোডের একটি আংশিক স্ক্রিনশট, যেখানে তীর চিহ্ন দিয়ে 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. সবকিছু ঠিকঠাক কাজ করছে কিনা তা যাচাই করতে এই কোডটি চালান। এতে শুধু একটি ফাঁকা কালো ব্যাকগ্রাউন্ডসহ একটি নতুন উইন্ডো প্রদর্শিত হবে। বিশ্বের সবচেয়ে বাজে ভিডিও গেমটি এখন ৬০ এফপিএস-এ রেন্ডার হচ্ছে!

একটি স্ক্রিনশট যেখানে ব্রিক_ব্রেকার অ্যাপ্লিকেশন উইন্ডোটি সম্পূর্ণ কালো দেখা যাচ্ছে।

৪. গেমটি তৈরি করুন

খেলাটির আকার নির্ধারণ করুন

দ্বিমাত্রিক (2D) খেলার জন্য একটি খেলার জায়গা প্রয়োজন। আপনি নির্দিষ্ট মাপের একটি জায়গা তৈরি করবেন এবং তারপর এই মাপগুলো ব্যবহার করে খেলার অন্যান্য দিকগুলোর আকার নির্ধারণ করবেন।

খেলার জায়গায় স্থানাঙ্ক বিন্যস্ত করার বিভিন্ন উপায় আছে। একটি প্রচলিত নিয়ম অনুযায়ী, স্ক্রিনের কেন্দ্র থেকে দিক পরিমাপ করা যায়, যেখানে মূলবিন্দু (0,0) স্ক্রিনের কেন্দ্রে অবস্থিত। ধনাত্মক মানগুলো x-অক্ষ বরাবর আইটেমগুলোকে ডানে এবং y-অক্ষ বরাবর উপরে সরায়। এই নিয়মটি আজকালকার বেশিরভাগ আধুনিক গেমের ক্ষেত্রে প্রযোজ্য, বিশেষ করে ত্রিমাত্রিক গেমগুলোর ক্ষেত্রে।

মূল ব্রেকআউট গেমটি তৈরির সময় প্রচলিত নিয়ম ছিল যে, মূলবিন্দুকে উপরের বাম কোণে স্থাপন করা হবে। ধনাত্মক x দিকটি একই ছিল, কিন্তু y দিকটি উল্টে দেওয়া হয়েছিল। ধনাত্মক x দিকটি ছিল ডানদিকে এবং y দিকটি ছিল নিচের দিকে। সেই যুগের রীতি বজায় রাখতে, এই গেমটির মূলবিন্দুও উপরের বাম কোণে স্থাপন করা হয়েছে।

lib/src নামের একটি নতুন ডিরেক্টরিতে config.dart নামে একটি ফাইল তৈরি করুন। পরবর্তী ধাপগুলোতে এই ফাইলে আরও কিছু কনস্ট্যান্ট যুক্ত করা হবে।

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;

এই গেমটি ৮২০ পিক্সেল চওড়া এবং ১৬০০ পিক্সেল উঁচু হবে। গেমের এলাকাটি যে উইন্ডোতে এটি প্রদর্শিত হয়, তার সাথে মানানসই করে আকার পরিবর্তন করে, কিন্তু স্ক্রিনে যুক্ত করা সমস্ত উপাদান এই উচ্চতা এবং প্রস্থ মেনে চলে।

একটি খেলার জায়গা তৈরি করুন

ব্রেকআউট গেমে, বলটি খেলার জায়গার দেয়াল থেকে বাউন্স করে। সংঘর্ষের বিষয়টি নিশ্চিত করতে, প্রথমে আপনার একটি 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);
  }
}

ফ্লাটারে যেখানে Widget ) আছে, ফ্লেমে সেখানে Component ) আছে। ফ্লাটার অ্যাপে যেখানে উইজেটের ট্রি তৈরি করতে হয়, ফ্লেম গেমে সেখানে কম্পোনেন্টের ট্রি রক্ষণাবেক্ষণ করতে হয়।

ফ্লাটার এবং ফ্লেমের মধ্যে এখানেই একটি আকর্ষণীয় পার্থক্য নিহিত। ফ্লাটারের উইজেট ট্রি হলো একটি ক্ষণস্থায়ী বর্ণনা, যা স্থায়ী এবং পরিবর্তনযোগ্য RenderObject লেয়ারকে আপডেট করার জন্য তৈরি করা হয়েছে। ফ্লেমের কম্পোনেন্টগুলো স্থায়ী এবং পরিবর্তনযোগ্য, এবং আশা করা হয় যে ডেভেলপার এই কম্পোনেন্টগুলোকে একটি সিমুলেশন সিস্টেমের অংশ হিসেবে ব্যবহার করবেন।

ফ্লেম-এর কম্পোনেন্টগুলো গেম মেকানিক্স প্রকাশের জন্য বিশেষভাবে তৈরি। এই কোডল্যাবটি গেম লুপ দিয়ে শুরু হবে, যা পরবর্তী ধাপে দেখানো হবে।

  1. অগোছালো অবস্থা নিয়ন্ত্রণে রাখতে, এই প্রোজেক্টের সমস্ত কম্পোনেন্ট সম্বলিত একটি ফাইল যোগ করুন। lib/src/components এ একটি components.dart ফাইল তৈরি করুন এবং নিম্নলিখিত বিষয়বস্তু যোগ করুন।

lib/src/components/components.dart

export 'play_area.dart';

export ডিরেক্টিভটি import -এর বিপরীত ভূমিকা পালন করে। অন্য কোনো ফাইলে ইম্পোর্ট করা হলে এই ফাইলটি কী কী কার্যকারিতা প্রকাশ করবে, তা এটি ঘোষণা করে। পরবর্তী ধাপগুলোতে আপনি নতুন কম্পোনেন্ট যোগ করার সাথে সাথে এই ফাইলটিতে আরও এন্ট্রি যুক্ত হবে।

একটি শিখা খেলা তৈরি করুন

পূর্ববর্তী ধাপের লাল আঁকাবাঁকা রেখাগুলো নিভিয়ে ফেলার জন্য, Flame-এর FlameGame এর জন্য একটি নতুন সাবক্লাস তৈরি করুন।

  1. lib/src ফোল্ডারে brick_breaker.dart নামে একটি ফাইল তৈরি করুন এবং নিম্নলিখিত কোডটি যোগ করুন।

lib/src/brick_breaker.dart

import 'dart:async';

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

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

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

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

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

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());
  }
}

এই ফাইলটি গেমের কার্যক্রম সমন্বয় করে। গেম ইনস্ট্যান্স তৈরির সময়, এই কোডটি গেমটিকে একটি নির্দিষ্ট রেজোলিউশনে রেন্ডারিং করার জন্য কনফিগার করে। গেমটি তার চারপাশের স্ক্রিনটি পূরণ করার জন্য আকার পরিবর্তন করে এবং প্রয়োজন অনুযায়ী লেটারবক্সিং যোগ করে।

আপনি গেমটির প্রস্থ এবং উচ্চতা প্রকাশ করেন, যাতে PlayArea মতো চাইল্ড কম্পোনেন্টগুলো নিজেদেরকে উপযুক্ত আকারে সেট করে নিতে পারে।

onLoad ওভাররাইড করা মেথডটিতে আপনার কোড দুটি কাজ সম্পাদন করে।

  1. ভিউফাইন্ডারের অ্যাঙ্কর হিসেবে উপরের বাম দিককে সেট করে। ডিফল্টরূপে, viewfinder (0,0) এর জন্য এলাকাটির মাঝখানকে অ্যাঙ্কর হিসেবে ব্যবহার করে।
  2. PlayArea world যুক্ত করে। ওয়ার্ল্ডটি গেম ওয়ার্ল্ডকে প্রতিনিধিত্ব করে। এটি CameraComponent এর ভিউ ট্রান্সফরমেশনের মাধ্যমে এর সমস্ত চাইল্ডকে প্রজেক্ট করে।

গেমটি স্ক্রিনে আনুন।

এই ধাপে আপনার করা সমস্ত পরিবর্তন দেখতে, নিম্নলিখিত পরিবর্তনগুলি সহ আপনার lib/main.dart ফাইলটি আপডেট করুন।

lib/main.dart

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

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

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

এই পরিবর্তনগুলো করার পর, গেমটি পুনরায় চালু করুন। গেমটি নিচের চিত্রটির মতো দেখাবে।

একটি স্ক্রিনশট যেখানে brick_breaker অ্যাপ্লিকেশন উইন্ডোটি দেখা যাচ্ছে এবং অ্যাপ উইন্ডোটির মাঝখানে একটি বালু রঙের আয়তক্ষেত্র রয়েছে।

পরবর্তী ধাপে, আপনি পৃথিবীতে একটি বল যোগ করবেন এবং এটিকে সচল করে তুলবেন!

৫. বলটি প্রদর্শন করুন।

বল উপাদানটি তৈরি করুন

স্ক্রিনে একটি চলমান বল রাখতে হলে আরেকটি উপাদান তৈরি করে সেটিকে গেম ওয়ার্ল্ডে যুক্ত করতে হয়।

  1. lib/src/config.dart ফাইলের বিষয়বস্তু নিম্নরূপভাবে সম্পাদনা করুন।

lib/src/config.dart

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

নামযুক্ত ধ্রুবককে ডিরাইভড ভ্যালু হিসেবে সংজ্ঞায়িত করার ডিজাইন প্যাটার্নটি এই কোডল্যাবে বহুবার ব্যবহৃত হবে। এর মাধ্যমে আপনি টপ লেভেলের gameWidth এবং gameHeight পরিবর্তন করে দেখতে পারবেন যে, এর ফলে গেমটির চেহারা ও অনুভূতিতে কী ধরনের পরিবর্তন আসে।

  1. lib/src/components ফোল্ডারের ball.dart নামের ফাইলে Ball কম্পোনেন্টটি তৈরি করুন।

lib/src/components/ball.dart

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

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

  final Vector2 velocity;

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

পূর্বে, আপনি RectangleComponent ব্যবহার করে PlayArea সংজ্ঞায়িত করেছিলেন, তাই এটা স্বাভাবিক যে আরও বিভিন্ন আকৃতি বিদ্যমান। RectangleComponent এর মতো CircleComponentPositionedComponent থেকে উদ্ভূত, ফলে আপনি স্ক্রিনে বলটির অবস্থান নির্ধারণ করতে পারেন। আরও গুরুত্বপূর্ণ বিষয় হলো, এর অবস্থান আপডেট করা যায়।

এই কম্পোনেন্টটি velocity বা সময়ের সাথে অবস্থানের পরিবর্তনের ধারণাটি উপস্থাপন করে। ভেলোসিটি একটি Vector2 অবজেক্ট, কারণ ভেলোসিটির মধ্যে গতি এবং দিক উভয়ই রয়েছে । অবস্থান আপডেট করার জন্য, update মেথডটি ওভাররাইড করুন, যা গেম ইঞ্জিন প্রতিটি ফ্রেমে কল করে। dt হলো পূর্ববর্তী ফ্রেম এবং এই ফ্রেমের মধ্যবর্তী সময়কাল। এটি আপনাকে বিভিন্ন ফ্রেম রেট (60hz বা 120hz) বা অতিরিক্ত গণনার কারণে দীর্ঘ ফ্রেমের মতো বিষয়গুলির সাথে মানিয়ে নিতে সাহায্য করে।

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

এই পরিবর্তনটি world Ball কম্পোনেন্টটি যুক্ত করে। বলটির position ডিসপ্লে এলাকার কেন্দ্রে সেট করার জন্য, কোডটি প্রথমে গেমের আকার অর্ধেক করে, কারণ Vector2 Vector2 একটি স্কেলার মান দ্বারা স্কেল করার জন্য অপারেটর ওভারলোড ( * এবং / ) রয়েছে।

বলের velocity নির্ধারণ করা আরও জটিল। এর উদ্দেশ্য হলো বলটিকে একটি যুক্তিসঙ্গত গতিতে এলোমেলো দিকে স্ক্রিনের নিচে নামানো। normalized মেথডটি কল করলে একটি Vector2 অবজেক্ট তৈরি হয়, যা মূল Vector2 এর মতোই একই দিকে সেট করা থাকে, কিন্তু এর দূরত্ব ১-এ স্কেল ডাউন করা হয়। এর ফলে বলটি যে দিকেই যাক না কেন, তার গতি স্থির থাকে। এরপর বলের বেগটিকে গেমের উচ্চতার ১/৪ অংশে স্কেল আপ করা হয়।

এই বিভিন্ন মানগুলোকে সঠিকভাবে নির্ধারণ করতে কিছু পুনরাবৃত্তির প্রয়োজন হয়, যা এই শিল্পে প্লেটেস্টিং নামেও পরিচিত।

শেষ লাইনটি ডিবাগিং ডিসপ্লে চালু করে, যা ডিবাগিং-এ সাহায্য করার জন্য ডিসপ্লেতে অতিরিক্ত তথ্য যোগ করে।

এখন যখন আপনি গেমটি চালাবেন, তখন এটি নিম্নলিখিত প্রদর্শনের মতো দেখাবে।

একটি স্ক্রিনশট যেখানে brick_breaker অ্যাপ্লিকেশন উইন্ডোর বালু রঙের আয়তক্ষেত্রের উপরে একটি নীল বৃত্ত দেখা যাচ্ছে। নীল বৃত্তটির চারপাশে সংখ্যা দিয়ে এর আকার এবং স্ক্রিনে এর অবস্থান চিহ্নিত করা হয়েছে।

PlayArea কম্পোনেন্ট এবং Ball কম্পোনেন্ট উভয়েরই ডিবাগিং তথ্য রয়েছে, কিন্তু ব্যাকগ্রাউন্ড ম্যাটগুলো PlayArea এর সংখ্যাগুলোকে ক্রপ করে দেয়। সবকিছুর ডিবাগিং তথ্য প্রদর্শিত হওয়ার কারণ হলো, আপনি সম্পূর্ণ কম্পোনেন্ট ট্রি-এর জন্য debugMode চালু করেছেন। যদি সেটি আরও বেশি সুবিধাজনক হয়, তবে আপনি শুধুমাত্র নির্বাচিত কম্পোনেন্টগুলোর জন্যও ডিবাগিং চালু করতে পারেন।

আপনি যদি আপনার গেমটি কয়েকবার রিস্টার্ট করেন, তাহলে হয়তো লক্ষ্য করবেন যে বলটি দেয়ালের সাথে ঠিক প্রত্যাশা অনুযায়ী আচরণ করছে না। এই প্রভাবটি আনার জন্য, আপনাকে কলিশন ডিটেকশন যোগ করতে হবে, যা আপনি পরবর্তী ধাপে করবেন।

৬. লাফালাফি করুন

সংঘর্ষ সনাক্তকরণ যোগ করুন

সংঘর্ষ শনাক্তকরণ এমন একটি বৈশিষ্ট্য যোগ করে, যার মাধ্যমে আপনার গেম দুটি বস্তুর পারস্পরিক সংস্পর্শ শনাক্ত করতে পারে।

গেমে সংঘর্ষ শনাক্তকরণ (collision detection) যোগ করতে, নিচের কোডে দেখানো অনুযায়ী BrickBreaker গেমে HasCollisionDetection মিক্সিনটি যুক্ত করুন।

lib/src/brick_breaker.dart

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

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

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

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

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

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

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

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

    debugMode = true;
  }
}

এটি কম্পোনেন্টগুলোর হিটবক্স ট্র্যাক করে এবং প্রতিটি গেম টিকে কলিশন কলব্যাক ট্রিগার করে।

গেমের হিটবক্সগুলো পূরণ করা শুরু করতে, দেখানো অনুযায়ী PlayArea কম্পোনেন্টটি পরিবর্তন করুন:

lib/src/components/play_area.dart

import 'dart:async';

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

import '../brick_breaker.dart';

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

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

RectangleComponent এর চাইল্ড হিসেবে একটি RectangleHitbox কম্পোনেন্ট যোগ করলে, সংঘর্ষ শনাক্তকরণের জন্য প্যারেন্ট কম্পোনেন্টের আকারের সাথে মেলে এমন একটি হিটবক্স তৈরি হবে। RectangleHitbox এর একটি ফ্যাক্টরি কনস্ট্রাক্টর রয়েছে, যার নাম relative । এটি তখন ব্যবহার করা হয় যখন আপনি প্যারেন্ট কম্পোনেন্টের চেয়ে ছোট বা বড় একটি হিটবক্স চান।

বলটি বাউন্স করুন

এখন পর্যন্ত, সংঘর্ষ শনাক্তকরণ যোগ করার ফলে গেমপ্লেতে কোনো পরিবর্তন আসেনি। Ball কম্পোনেন্টটি পরিবর্তন করলেই এতে পরিবর্তন আসে। PlayArea সাথে সংঘর্ষ হলে বলটির আচরণ পরিবর্তন করা প্রয়োজন।

Ball কম্পোনেন্টটি নিম্নরূপভাবে পরিবর্তন করুন।

lib/src/components/ball.dart

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

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

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

  final Vector2 velocity;

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

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

এই উদাহরণটিতে onCollisionStart কলব্যাকটি যোগ করার মাধ্যমে একটি বড় পরিবর্তন আনা হয়েছে। পূর্ববর্তী উদাহরণে BrickBreaker এ যোগ করা সংঘর্ষ শনাক্তকরণ সিস্টেমটি এই কলব্যাকটিকে কল করে।

প্রথমে, কোডটি পরীক্ষা করে দেখে যে Ball PlayArea সাথে ধাক্কা খেয়েছে কিনা। আপাতত এটি অপ্রয়োজনীয় মনে হতে পারে, কারণ গেম ওয়ার্ল্ডে অন্য কোনো কম্পোনেন্ট নেই। পরবর্তী ধাপে, যখন আপনি ওয়ার্ল্ডে একটি ব্যাট যোগ করবেন, তখন এটি পরিবর্তিত হবে। এরপর, বলটি ব্যাট ছাড়া অন্য কিছুর সাথে ধাক্কা খেলে তা সামলানোর জন্য এটি একটি else কন্ডিশনও যোগ করে। বাকি লজিকগুলো ইমপ্লিমেন্ট করার জন্য এটি একটি মৃদু অনুস্মারক, যদি আপনি চান।

যখন বলটি নিচের দেয়ালে ধাক্কা খায়, তখন এটি খেলার পৃষ্ঠ থেকে অদৃশ্য হয়ে যায়, যদিও তখনও এটি পুরোপুরি দৃশ্যমান থাকে। আপনি পরবর্তী ধাপে ফ্লেমের এফেক্টস-এর শক্তি ব্যবহার করে এই আর্টিফ্যাক্টটি মোকাবেলা করবেন।

এখন যেহেতু বলটা খেলার দেয়ালের সাথে ধাক্কা খাচ্ছে, খেলোয়াড়কে বলটা মারার জন্য একটা ব্যাট দিলে বেশ কাজের হতো...

৭. ব্যাট দিয়ে বল লাগান

ব্যাট তৈরি করুন

খেলার মধ্যে বলকে খেলার মধ্যে রাখার জন্য একটি ব্যাট যোগ করতে,

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

এই কম্পোনেন্টটি কয়েকটি নতুন সক্ষমতা যোগ করে।

প্রথমত, Bat কম্পোনেন্টটি একটি PositionComponent , RectangleComponent বা CircleComponent নয়। এর মানে হলো, এই কোডটিকে স্ক্রিনে Bat রেন্ডার করতে হবে। এটি করার জন্য, কোডটি render কলব্যাককে ওভাররাইড করে।

canvas.drawRRect (গোলাকার আয়তক্ষেত্র আঁকার জন্য) কলটি ভালোভাবে দেখলে, আপনি নিজেকে প্রশ্ন করতে পারেন, "আয়তক্ষেত্রটি কোথায়?" Offset.zero & size.toSize() ফাংশনটি dart:ui Offset ক্লাসের একটি operator & ওভারলোড ব্যবহার করে, যা Rect তৈরি করে। এই সংক্ষিপ্ত রূপটি প্রথমে আপনাকে বিভ্রান্ত করতে পারে, কিন্তু আপনি এটি নিম্ন-স্তরের Flutter এবং Flame কোডে প্রায়শই দেখতে পাবেন।

দ্বিতীয়ত, এই Bat কম্পোনেন্টটি প্ল্যাটফর্মের উপর নির্ভর করে আঙুল বা মাউস দিয়ে ড্র্যাগ করা যায়। এই কার্যকারিতাটি বাস্তবায়ন করতে, আপনাকে DragCallbacks মিক্সিনটি যোগ করতে হবে এবং onDragUpdate ইভেন্টটি ওভাররাইড করতে হবে।

সবশেষে, Bat কম্পোনেন্টটিকে কিবোর্ড কন্ট্রোলে সাড়া দিতে হবে। ` moveBy ফাংশনটি অন্য কোডকে এই ব্যাটটিকে একটি নির্দিষ্ট সংখ্যক ভার্চুয়াল পিক্সেল বামে বা ডানে সরানোর নির্দেশ দিতে সাহায্য করে। এই ফাংশনটি ফ্লেম গেম ইঞ্জিনের একটি নতুন সক্ষমতা নিয়ে আসে: ` Effect । এই কম্পোনেন্টের চাইল্ড হিসেবে MoveToEffect অবজেক্টটি যোগ করার মাধ্যমে, প্লেয়ার ব্যাটটিকে একটি নতুন অবস্থানে অ্যানিমেটেড হতে দেখে। ফ্লেমে বিভিন্ন ধরনের ইফেক্ট প্রয়োগ করার জন্য বেশ কিছু Effect উপলব্ধ রয়েছে।

Effect-এর কনস্ট্রাক্টর আর্গুমেন্টগুলোতে game গেটারের একটি রেফারেন্স অন্তর্ভুক্ত থাকে। এই কারণেই আপনি এই ক্লাসে HasGameReference মিক্সিনটি অন্তর্ভুক্ত করেন। এই মিক্সিনটি কম্পোনেন্ট ট্রি-এর শীর্ষে থাকা BrickBreaker ইনস্ট্যান্সটি অ্যাক্সেস করার জন্য এই কম্পোনেন্টে একটি টাইপ-সেফ game অ্যাক্সেসর যোগ করে।

  1. BrickBreakerBat উপলব্ধ করতে, lib/src/components/components.dart ফাইলটি নিম্নরূপভাবে আপডেট করুন।

lib/src/components/components.dart

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

বিশ্বে ব্যাট যোগ করুন

গেম ওয়ার্ল্ডে Bat কম্পোনেন্টটি যোগ করতে, BrickBreaker নিম্নোক্তভাবে আপডেট করুন।

lib/src/brick_breaker.dart

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

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

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

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

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

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

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

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

    world.add(                                                  // 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 উপাংশকেও এমনভাবে আপডেট করে যা সংস্পর্শের মুহূর্তে ব্যাট ও বলের আপেক্ষিক অবস্থানের উপর নির্ভর করে। এটি খেলোয়াড়কে বলের গতিবিধির উপর আরও বেশি নিয়ন্ত্রণ দেয়, কিন্তু ঠিক কীভাবে তা ঘটবে, তা খেলা ছাড়া অন্য কোনোভাবে খেলোয়াড়কে জানানো হয় না।

এখন যেহেতু তোমার কাছে বল মারার জন্য একটি ব্যাট আছে, তাহলে বল দিয়ে কিছু ইট ভাঙতে পারলে বেশ ভালো হতো!

৮. দেয়ালটি ভেঙে ফেলুন

ইটগুলো তৈরি করুন

খেলায় ইট যোগ করতে,

  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 গেমের একটি টাইপ-সেফ রেফারেন্স উভয়ই রয়েছে।

এই কোডটির সবচেয়ে গুরুত্বপূর্ণ নতুন ধারণাটি হলো, খেলোয়াড় কীভাবে জয়ের শর্তটি পূরণ করে। জয়ের শর্ত যাচাই করার জন্য, এটি গেমের জগতে থাকা ইটগুলোর খোঁজ নেয় এবং নিশ্চিত করে যে কেবল একটিই অবশিষ্ট আছে। বিষয়টি কিছুটা বিভ্রান্তিকর হতে পারে, কারণ এর আগের লাইনটি এই ইটটিকে তার মূল ইটটি থেকে সরিয়ে দেয়।

বোঝার মূল বিষয়টি হলো, কম্পোনেন্ট রিমুভাল একটি কিউড কমান্ড। এটি এই কোডটি চলার পরে, কিন্তু গেম ওয়ার্ল্ডের পরবর্তী টিকের আগে ব্রিকটি সরিয়ে দেয়।

BrickBreaker এর জন্য Brick কম্পোনেন্টটি অ্যাক্সেসযোগ্য করতে, 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;
  }
}

গেমটি চালালে এর সমস্ত মূল কার্যপ্রণালী দেখা যায়। আপনি ডিবাগিং বন্ধ করে দিয়ে কাজ শেষ করতে পারেন, কিন্তু তবুও কিছু একটা যেন কমতি থেকে যায়।

একটি স্ক্রিনশট যেখানে খেলার জায়গায় বল, ব্যাট এবং বেশিরভাগ ইটসহ ব্রিক_ব্রেকার দেখানো হচ্ছে। প্রতিটি উপাদানে ডিবাগিং লেবেল রয়েছে।

একটি ওয়েলকাম স্ক্রিন, একটি গেম ওভার স্ক্রিন এবং স্কোর থাকলে কেমন হয়? ফ্লাটার গেমে এই ফিচারগুলো যোগ করতে পারে, এবং এরপরে আপনার মনোযোগ সেদিকেই যাবে।

৯. খেলাটি জিতুন।

খেলার অবস্থা যোগ করুন

এই ধাপে, আপনি ফ্লেম গেমটিকে একটি ফ্লাটার র‍্যাপারের ভেতরে এমবেড করবেন এবং তারপর ওয়েলকাম, গেম ওভার ও ওন স্ক্রিনগুলোর জন্য ফ্লাটার ওভারলে যোগ করবেন।

প্রথমে, আপনাকে গেম এবং কম্পোনেন্ট ফাইলগুলো পরিবর্তন করে এমন একটি প্লে স্টেট প্রয়োগ করতে হবে, যা নির্দেশ করবে যে কোনো ওভারলে দেখানো হবে কি না এবং যদি দেখানো হয়, তবে কোনটি।

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

অন্যদিকে, খেলোয়াড় যদি সবগুলো ইট ভাঙতে পারে, তাহলে সে "গেম জয়" স্ক্রিনটি পাবে। সাবাশ খেলোয়াড়, সাবাশ!

ফ্লাটার র‍্যাপার যোগ করুন

গেমটি এমবেড করার এবং প্লে স্টেট ওভারলে যোগ করার জন্য, ফ্লাটার শেলটি যুক্ত করুন।

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

এই ফাইলের বেশিরভাগ বিষয়বস্তু একটি স্ট্যান্ডার্ড ফ্লাটার উইজেট ট্রি গঠন অনুসরণ করে। ফ্লেম-এর জন্য নির্দিষ্ট অংশগুলোর মধ্যে রয়েছে 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 প্রদর্শনের জন্য আপনাকে শেষবারের মতো একটি ছোটখাটো পরিবর্তন করতে হবে।

ফন্ট অ্যাক্সেস সক্ষম করুন

অ্যান্ড্রয়েডের জন্য ইন্টারনেট অনুমতি যোগ করুন

অ্যান্ড্রয়েডের জন্য আপনাকে ইন্টারনেট পারমিশন যোগ করতে হবে। আপনার 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-এর জন্য এনটাইটেলমেন্ট ফাইলগুলি সম্পাদনা করুন

ম্যাকওএস-এর জন্য আপনাকে দুটি ফাইল সম্পাদনা করতে হবে।

  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>

এটি সরাসরি চালালে সব প্ল্যাটফর্মে একটি স্বাগত স্ক্রিন এবং একটি গেম ওভার বা জয়ের স্ক্রিন দেখানো উচিত। স্ক্রিনগুলো হয়তো একটু সাদামাটা হতে পারে এবং একটি স্কোর থাকলে ভালো হতো। সুতরাং, অনুমান করুন তো পরের ধাপে আপনি কী করতে চলেছেন!

১০. স্কোর রাখুন

খেলায় স্কোর যোগ করুন

এই ধাপে, আপনি গেমের স্কোরকে পারিপার্শ্বিক ফ্লাটার কনটেক্সটে প্রকাশ করেন। এই ধাপে আপনি ফ্লেম গেম থেকে স্টেটকে পারিপার্শ্বিক ফ্লাটার স্টেট ম্যানেজমেন্টে প্রকাশ করেন। এর ফলে, প্লেয়ার যখনই একটি ইট ভাঙে, গেম কোড স্কোর আপডেট করতে পারে।

  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 যোগ করার মাধ্যমে, আপনি গেমের অবস্থাকে ফ্লাটার স্টেট ম্যানেজমেন্টের সাথে সংযুক্ত করেন।

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

একটি সুন্দর দেখতে গেম তৈরি করুন

এখন যেহেতু আপনি ফ্লাটারে স্কোর রাখতে পারেন, তাই এটিকে সুন্দর করে তোলার জন্য উইজেটগুলো সাজিয়ে নেওয়ার সময় এসেছে।

  1. lib/src/widgetsscore_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/widgetsoverlay_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 এর ক্ষমতা সম্পর্কে আরও গভীরভাবে জানতে, "Building next generation UIs in 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.
              ),
            ),
          ),
        ),
      ),
    );
  }
}

এই সবকিছু প্রস্তুত হয়ে গেলে, আপনি এখন ছয়টি ফ্লাটার টার্গেট প্ল্যাটফর্মের যেকোনোটিতে এই গেমটি চালাতে পারবেন। গেমটি দেখতে নিচের মতো হবে।

A screenshot of brick_breaker showing the pre-game screen inviting the user to tap the screen to play the game

A screenshot of brick_breaker showing the game over screen overlaid on top of a bat and some of the bricks

১১. অভিনন্দন

Congratulations, you succeeded in building a game with Flutter and Flame!

You built a game using the Flame 2D game engine and embedded it in a Flutter wrapper. You used Flame's Effects to animate and remove components. You used Google Fonts and Flutter Animate packages to make the whole game look well designed.

এরপর কী?

Check out some of these codelabs...

আরও পড়ুন