আমি সবসময় ভিডিও গেম বানাতে চেয়েছিলাম। আমার প্রথম অ্যান্ড্রয়েড অ্যাপ যেটি আমাকে আমার প্রথম কাজ পেতে সাহায্য করেছিল সেটি ছিল একটি সাধারণ গেম, যা অ্যান্ড্রয়েড ভিউ দিয়ে তৈরি। এর পরে, গেম ইঞ্জিন ব্যবহার করে আরও বিস্তৃত গেম তৈরি করার অনেক প্রচেষ্টা হয়েছিল, কিন্তু সময়ের অভাব বা কাঠামোর জটিলতার কারণে সেগুলি সবই ব্যর্থ হয়েছিল। কিন্তু যখন আমি প্রথম ফ্লাটারের উপর ভিত্তি করে ফ্লেম ইঞ্জিনের কথা শুনি, তখন আমি এর সরলতা এবং ক্রস-প্ল্যাটফর্ম সমর্থন দ্বারা আকৃষ্ট হয়েছিলাম, তাই আমি এটির সাথে একটি গেম তৈরি করার চেষ্টা করার সিদ্ধান্ত নিয়েছিলাম।
আমি ইঞ্জিনের অনুভূতি পেতে সহজ কিছু দিয়ে শুরু করতে চেয়েছিলাম, কিন্তু এখনও চ্যালেঞ্জিং। প্রবন্ধের এই সিরিজটি হল আমার ফ্লেম (এবং ফ্লাটার) শেখার এবং একটি মৌলিক প্ল্যাটফর্মার গেম তৈরি করার যাত্রা। আমি এটিকে বেশ বিশদভাবে তৈরি করার চেষ্টা করব, তাই এটি যে কারো জন্য উপযোগী হওয়া উচিত যারা কেবলমাত্র তাদের পায়ের আঙ্গুলগুলি ফ্লেম বা গেম ডেভের মধ্যে ডুবিয়ে দিচ্ছেন।
4টি নিবন্ধের মধ্যে, আমি একটি 2d সাইড-স্ক্রলিং গেম তৈরি করতে যাচ্ছি, যার মধ্যে রয়েছে:
একটি চরিত্র যা দৌড়াতে এবং লাফ দিতে পারে
একটি ক্যামেরা যা প্লেয়ারকে অনুসরণ করে
গ্রাউন্ড এবং প্ল্যাটফর্ম সহ স্ক্রোলিং লেভেল ম্যাপ
প্যারালাক্স ব্যাকগ্রাউন্ড
কয়েন যা প্লেয়ার সংগ্রহ করতে পারে এবং HUD যা কয়েনের সংখ্যা প্রদর্শন করে
পর্দা জয়
প্রথম অংশে, আমরা একটি নতুন ফ্লেম প্রকল্প তৈরি করতে যাচ্ছি, সমস্ত সম্পদ লোড করব, একজন খেলোয়াড়ের চরিত্র যোগ করব এবং তাকে কীভাবে চালাতে হবে তা শেখাবো।
প্রথমত, একটি নতুন প্রকল্প তৈরি করা যাক। অফিসিয়াল বেয়ার ফ্লেম গেম টিউটোরিয়ালটি এটি করার সমস্ত পদক্ষেপ বর্ণনা করার জন্য একটি দুর্দান্ত কাজ করে, তাই এটি অনুসরণ করুন।
যোগ করার জন্য একটি জিনিস: আপনি যখন pubspec.yaml
ফাইল সেট আপ করছেন, আপনি লাইব্রেরি সংস্করণগুলিকে সর্বশেষ উপলব্ধ সংস্করণে আপডেট করতে পারেন, বা এটিকে যেমন আছে তেমনই রেখে দিতে পারেন, কারণ একটি সংস্করণের আগে ক্যারেট সাইন (^) নিশ্চিত করবে যে আপনার অ্যাপটি সর্বশেষ নন ব্যবহার করে -ব্রেকিং সংস্করণ। ( ক্যারেট সিনট্যাক্স )
আপনি যদি সমস্ত পদক্ষেপ অনুসরণ করেন তবে আপনার main.dart
ফাইলটি এইরকম হওয়া উচিত:
import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); }
আমরা চালিয়ে যাওয়ার আগে, গেমের জন্য ব্যবহার করা হবে এমন সম্পদ প্রস্তুত করতে হবে। সম্পদ হল ছবি, অ্যানিমেশন, শব্দ, ইত্যাদি
একটি প্ল্যাটফর্মার স্তর তৈরি করার সবচেয়ে সহজ উপায় হল টাইল মানচিত্র এবং টাইল স্প্রিট ব্যবহার করা। এর মানে হল যে স্তরটি মূলত একটি গ্রিড, যেখানে প্রতিটি সেল নির্দেশ করে কোন বস্তু/গ্রাউন্ড/প্ল্যাটফর্মটি প্রতিনিধিত্ব করে। পরে, যখন গেমটি চলছে, প্রতিটি ঘর থেকে তথ্য সংশ্লিষ্ট টাইল স্প্রাইটে ম্যাপ করা হয়।
এই কৌশলটি ব্যবহার করে নির্মিত গেম গ্রাফিক্স সত্যিই বিস্তৃত বা খুব সহজ হতে পারে। উদাহরণস্বরূপ, সুপার মারিও ব্রোসে, আপনি দেখতে পাচ্ছেন যে প্রচুর উপাদান পুনরাবৃত্তি হচ্ছে। এর কারণ, গেম গ্রিডে প্রতিটি গ্রাউন্ড টাইলের জন্য, শুধুমাত্র একটি গ্রাউন্ড ইমেজ এটিকে উপস্থাপন করে। আমরা একই পদ্ধতি অনুসরণ করব এবং আমাদের কাছে থাকা প্রতিটি স্থির বস্তুর জন্য একটি একক চিত্র প্রস্তুত করব।
আমরা কিছু বস্তুও চাই, যেমন প্লেয়ার চরিত্র এবং কয়েন অ্যানিমেটেড হোক। অ্যানিমেশন সাধারণত স্থির চিত্রগুলির একটি সিরিজ হিসাবে সংরক্ষণ করা হয়, প্রতিটি একটি একক ফ্রেমের প্রতিনিধিত্ব করে। যখন অ্যানিমেশন বাজানো হয়, ফ্রেমগুলি একের পর এক চলে যায়, বস্তুর চলমানতার বিভ্রম তৈরি করে।
এখন সবচেয়ে গুরুত্বপূর্ণ প্রশ্ন হল সম্পদ কোথায় পাওয়া যাবে। অবশ্যই, আপনি সেগুলি নিজেই আঁকতে পারেন বা কোনও শিল্পীর কাছে তাদের কমিশন করতে পারেন। এছাড়াও, অনেক দুর্দান্ত শিল্পী আছেন যারা ওপেন সোর্সে গেমের সম্পদগুলি অবদান রেখেছেন। আমি GrafxKid- এর Arcade Platformer Assets প্যাক ব্যবহার করব।
সাধারণত, ইমেজ সম্পদ দুটি আকারে আসে: স্প্রাইট শীট এবং একক স্প্রাইট। প্রাক্তনটি একটি বৃহৎ চিত্র, একটিতে সমস্ত গেম সম্পদ রয়েছে৷ তারপরে গেম ডেভেলপাররা প্রয়োজনীয় স্প্রাইটের সঠিক অবস্থান নির্দিষ্ট করে এবং গেম ইঞ্জিন এটিকে শীট থেকে কেটে দেয়। এই গেমটির জন্য, আমি একক স্প্রাইট ব্যবহার করব (অ্যানিমেশন বাদে, তাদের একটি চিত্র হিসাবে রাখা সহজ) কারণ আমার স্প্রাইট শীটে দেওয়া সমস্ত সম্পদের প্রয়োজন নেই।
আপনি নিজে স্প্রাইট তৈরি করছেন বা সেগুলি কোনও শিল্পীর কাছ থেকে নিয়ে আসছেন, গেম ইঞ্জিনের জন্য সেগুলিকে আরও উপযুক্ত করার জন্য আপনাকে সেগুলিকে টুকরো টুকরো করতে হবে৷ আপনি সেই উদ্দেশ্যে বিশেষভাবে তৈরি করা সরঞ্জামগুলি ব্যবহার করতে পারেন (যেমন টেক্সচার প্যাকার) বা কোনও গ্রাফিকাল সম্পাদক। আমি অ্যাডোব ফটোশপ ব্যবহার করেছি, কারণ, এই স্প্রাইট শীটে, স্প্রাইটগুলির মধ্যে অসম স্থান রয়েছে, যা স্বয়ংক্রিয় সরঞ্জামগুলির জন্য চিত্রগুলি বের করা কঠিন করে তুলেছিল, তাই আমাকে এটি ম্যানুয়ালি করতে হয়েছিল।
আপনি সম্পদের আকার বাড়াতেও চাইতে পারেন, কিন্তু যদি এটি একটি ভেক্টর চিত্র না হয়, ফলে স্প্রাইটটি ঝাপসা হয়ে যেতে পারে। একটি সমাধান আমি খুঁজে পেয়েছি যে পিক্সেল শিল্পের জন্য দুর্দান্ত কাজ করে তা হল ফটোশপে Nearest Neighbour (hard edges)
রিসাইজিং পদ্ধতি ব্যবহার করা (বা জিম্পে ইন্টারপোলেশন সেট করা নেই)। কিন্তু যদি আপনার সম্পদ আরো বিস্তারিত হয়, তাহলে সম্ভবত এটি কাজ করবে না।
ব্যাখ্যা ছাড়া, আমি যে সম্পদগুলি তৈরি করেছি তা ডাউনলোড করুন বা আপনার নিজের তৈরি করুন এবং আপনার প্রকল্পের assets/images
ফোল্ডারে যোগ করুন।
যখনই আপনি নতুন সম্পদ যোগ করবেন, আপনাকে সেগুলিকে pubspec.yaml
ফাইলে এইভাবে নিবন্ধন করতে হবে:
flutter: assets: - assets/images/
এবং ভবিষ্যতের জন্য টিপ: আপনি যদি ইতিমধ্যে নিবন্ধিত সম্পদগুলি আপডেট করেন তবে পরিবর্তনগুলি দেখতে আপনাকে গেমটি পুনরায় চালু করতে হবে৷
এখন এর প্রকৃতপক্ষে খেলার মধ্যে সম্পদ লোড করা যাক. আমি সমস্ত সম্পদের নাম এক জায়গায় রাখতে চাই, যা একটি ছোট গেমের জন্য দুর্দান্ত কাজ করে, কারণ সবকিছুর ট্র্যাক রাখা এবং প্রয়োজনে পরিবর্তন করা সহজ। সুতরাং, আসুন lib
ডিরেক্টরিতে একটি নতুন ফাইল তৈরি করি: assets.dart
const String THE_BOY = "theboy.png"; const String GROUND = "ground.png"; const String PLATFORM = "platform.png"; const String MIST = "mist.png"; const String CLOUDS = "clouds.png"; const String HILLS = "hills.png"; const String COIN = "coin.png"; const String HUD = "hud.png"; const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD];
এবং তারপরে অন্য একটি ফাইল তৈরি করুন, যাতে ভবিষ্যতে সমস্ত গেম লজিক থাকবে: game.dart
import 'package:flame/game.dart'; import 'assets.dart' as Assets; class PlatformerGame extends FlameGame { @override Future<void> onLoad() async { await images.loadAll(Assets.SPRITES); } }
PlatformerGame
হল প্রধান শ্রেণী যা আমাদের গেমের প্রতিনিধিত্ব করে, এটি FlameGame
প্রসারিত করে, ফ্লেম ইঞ্জিনে ব্যবহৃত বেস গেম ক্লাস। যা ঘুরে Component
প্রসারিত করে - শিখার মৌলিক বিল্ডিং ব্লক। ছবি, ইন্টারফেস বা প্রভাব সহ আপনার গেমের সবকিছুই উপাদান। প্রতিটি Component
একটি async পদ্ধতি আছে onLoad
, যাকে কম্পোনেন্ট ইনিশিয়ালাইজেশন বলা হয়। সাধারণত, সমস্ত উপাদান সেটআপ যুক্তি সেখানে যায়।
অবশেষে, আমরা আমাদের assets.dart
ফাইলটি আমদানি করেছি যা আমরা আগে তৈরি করেছি এবং আমাদের সম্পদের ধ্রুবকগুলি কোথা থেকে আসছে তা স্পষ্টভাবে ঘোষণা করার জন্য as Assets
যোগ করেছি। এবং SPRITES
তালিকায় তালিকাভুক্ত সমস্ত সম্পদ গেম ইমেজ ক্যাশে লোড করতে images.loadAll
ব্যবহার করা হয়েছে।
তারপর, আমাদের main.dart
থেকে আমাদের নতুন PlatformerGame
তৈরি করতে হবে। ফাইলটি নিম্নরূপ পরিবর্তন করুন:
import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; import 'game.dart'; void main() { runApp( const GameWidget<PlatformerGame>.controlled( gameFactory: PlatformerGame.new, ), ); }
সমস্ত প্রস্তুতি সম্পন্ন হয়, এবং মজার অংশ শুরু হয়।
একটি নতুন ফোল্ডার lib/actors/
এবং এর ভিতরে একটি নতুন ফাইল theboy.dart
তৈরি করুন। এটি এমন একটি উপাদান হতে চলেছে যা খেলোয়াড়ের চরিত্রের প্রতিনিধিত্ব করে: দ্য বয়।
import '../game.dart'; import '../assets.dart' as Assets; import 'package:flame/components.dart'; class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { TheBoy({ required super.position, // Position on the screen }) : super( size: Vector2.all(48), // Size of the component anchor: Anchor.bottomCenter // ); @override Future<void> onLoad() async { animation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, // For now we only need idle animation, so we load only 1 frame textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet stepTime: 0.12, // Time between frames, since it's a single frame not that important ), ); } }
ক্লাসটি SpriteAnimationComponent
প্রসারিত করে যা অ্যানিমেটেড স্প্রাইটগুলির জন্য ব্যবহৃত একটি উপাদান এবং এতে একটি মিক্সিন HasGameRef
রয়েছে যা গেমের ক্যাশে থেকে ছবি লোড করতে বা পরে গ্লোবাল ভেরিয়েবল পেতে গেম অবজেক্টের রেফারেন্স করতে দেয়।
আমাদের onLoad
পদ্ধতিতে আমরা assets.dart
ফাইলে ঘোষিত THE_BOY
স্প্রাইট শীট থেকে একটি নতুন SpriteAnimation
তৈরি করি।
এখন খেলায় আমাদের খেলোয়াড় যোগ করা যাক! game.dart
ফাইলে ফিরে যান এবং onLoad
পদ্ধতির নীচে নিম্নলিখিত যোগ করুন:
final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy);
আপনি যদি এখন গেমটি চালান, তাহলে আমরা ছেলেটির সাথে দেখা করতে সক্ষম হব!
প্রথমত, আমাদের কীবোর্ড থেকে দ্য বয়কে নিয়ন্ত্রণ করার ক্ষমতা যোগ করতে হবে। game.dart
ফাইলে HasKeyboardHandlerComponents
mixin যোগ করা যাক।
class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents
এর পরে, আসুন theboy.dart
এবং KeyboardHandler
মিশ্রণে ফিরে যাই:
class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame>
তারপরে, TheBoy
কম্পোনেন্টে কিছু নতুন ক্লাস ভেরিয়েবল যোগ করুন:
final double _moveSpeed = 300; // Max player's move speed int _horizontalDirection = 0; // Current direction the player is facing final Vector2 _velocity = Vector2.zero(); // Current player's speed
সবশেষে, আসুন onKeyEvent
পদ্ধতিটিকে ওভাররাইড করি যা কীবোর্ড ইনপুট শোনার অনুমতি দেয়:
@override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { _horizontalDirection = 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; }
এখন _horizontalDirection
সমান 1 যদি প্লেয়ার ডানে চলে যায়, -1 যদি প্লেয়ার বাম দিকে চলে যায়, এবং 0 যদি প্লেয়ার সরে না যায়। যাইহোক, আমরা এখনও এটি স্ক্রিনে দেখতে পাচ্ছি না, কারণ প্লেয়ারের অবস্থান এখনও পরিবর্তিত হয়নি। update
পদ্ধতি যোগ করে এটি ঠিক করা যাক।
এখন আমি গেম লুপ কি ব্যাখ্যা করতে হবে. মূলত, এর মানে হল যে গেমটি একটি অন্তহীন লুপে চালানো হচ্ছে। প্রতিটি পুনরাবৃত্তিতে, Component's
পদ্ধতি render
বর্তমান অবস্থা রেন্ডার করা হয় এবং তারপর পদ্ধতি update
একটি নতুন অবস্থা গণনা করা হয়। পদ্ধতির স্বাক্ষরে dt
প্যারামিটারটি শেষ স্টেট আপডেটের পর থেকে মিলিসেকেন্ডে সময়। এটি মাথায় রেখে, theboy.dart
এ নিম্নলিখিতটি যুক্ত করুন:
@override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; }
প্রতিটি গেম লুপ চক্রের জন্য, আমরা বর্তমান দিক এবং সর্বোচ্চ গতি ব্যবহার করে অনুভূমিক বেগ আপডেট করি। তারপরে আমরা dt
দ্বারা গুণিত আপডেট করা মান দিয়ে স্প্রাইট অবস্থান পরিবর্তন করি।
কেন আমরা শেষ অংশ প্রয়োজন? ঠিক আছে, আপনি যদি ঠিক বেগের সাথে অবস্থানটি আপডেট করেন তবে স্প্রাইটটি মহাকাশে উড়ে যাবে। কিন্তু আমরা কি শুধু ছোট গতির মান ব্যবহার করতে পারি, আপনি জিজ্ঞাসা করতে পারেন? আমরা পারি, কিন্তু প্লেয়ারের চলাফেরা করার পদ্ধতি ভিন্ন ভিন্ন ফ্রেম প্রতি সেকেন্ডে (FPS) হারে ভিন্ন হবে। প্রতি সেকেন্ডে ফ্রেমের সংখ্যা (বা গেম লুপ) গেমের পারফরম্যান্স এবং এটি চালানো হার্ডওয়্যারের উপর নির্ভর করে। ডিভাইসের পারফরম্যান্স যত ভাল হবে, FPS তত বেশি হবে এবং প্লেয়ার তত দ্রুত চলে যাবে। এটি এড়ানোর জন্য, আমরা গতিকে শেষ ফ্রেম থেকে পাস করা সময়ের উপর নির্ভর করি। এইভাবে স্প্রাইট যেকোন FPS-এ একইভাবে চলে যাবে।
ঠিক আছে, যদি আমরা এখন গেমটি চালাই, আমাদের এটি দেখতে হবে:
আশ্চর্যজনক, এখন বাম দিকে গেলে ছেলেটিকে ঘুরিয়ে দেই। update
পদ্ধতির নীচে এটি যোগ করুন:
if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); }
মোটামুটি সহজ যুক্তি: আমরা পরীক্ষা করি যে বর্তমান দিকটি (ব্যবহারকারী যে তীরটি টিপেছে) স্প্রাইটের দিক থেকে ভিন্ন কিনা, তারপর আমরা অনুভূমিক অক্ষ বরাবর স্প্রাইটটিকে উল্টিয়ে দেই।
এখন চলমান অ্যানিমেশন যোগ করা যাক। প্রথমে দুটি নতুন ক্লাস ভেরিয়েবল সংজ্ঞায়িত করুন:
late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation;
তারপর এই মত onLoad
আপডেট করুন:
@override Future<void> onLoad() async { _idleAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, textureSize: Vector2.all(20), stepTime: 0.12, ), ); _runAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(20), stepTime: 0.12, ), ); animation = _idleAnimation; }
এখানে আমরা ক্লাস ভেরিয়েবলে পূর্বে যোগ করা নিষ্ক্রিয় অ্যানিমেশন বের করেছি এবং একটি নতুন রান অ্যানিমেশন ভেরিয়েবল সংজ্ঞায়িত করেছি।
এর পরে, আসুন একটি নতুন updateAnimation
পদ্ধতি যুক্ত করি:
void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } }
এবং অবশেষে, update
পদ্ধতির নীচে এই পদ্ধতিটি চালু করুন এবং গেমটি চালান।
প্রথম অংশের জন্য এটাই। আমরা শিখেছি কিভাবে একটি ফ্লেম গেম সেট আপ করতে হয়, কোথায় সম্পদ খুঁজে বের করতে হয়, কীভাবে সেগুলি আপনার গেমে লোড করতে হয় এবং কীভাবে একটি দুর্দান্ত অ্যানিমেটেড চরিত্র তৈরি করতে হয় এবং কীবোর্ড ইনপুটগুলির উপর ভিত্তি করে এটিকে সরানো যায়। এই অংশের জন্য কোডটি আমার গিথুবে পাওয়া যেতে পারে।
পরের প্রবন্ধে, আমি কভার করব কীভাবে টাইলড ব্যবহার করে একটি গেম লেভেল তৈরি করা যায়, কীভাবে ফ্লেম ক্যামেরা নিয়ন্ত্রণ করা যায় এবং একটি প্যারালাক্স ব্যাকগ্রাউন্ড যোগ করা যায়। সাথে থাকুন!
প্রতিটি অংশের শেষে, আমি অসাধারণ সৃষ্টিকর্তা এবং সম্পদের একটি তালিকা যোগ করব যা থেকে আমি শিখেছি।