একটি সাম্প্রতিক সমীক্ষা অনুসারে, 5টির মধ্যে মাত্র 2টি স্টার্টআপ লাভজনক। একটি MVP (ন্যূনতম কার্যকর পণ্য) উল্লেখযোগ্যভাবে একটি স্টার্টআপের লাভের সম্ভাবনা বাড়ায় কারণ এটি এই ধরনের ব্যবসাগুলিকে সম্পূর্ণ কার্যকারিতা সহ একটি অ্যাপে সম্পূর্ণ বাজেট ব্যয় না করে প্রাথমিক ব্যবহারকারীর প্রতিক্রিয়া সংগ্রহ করতে দেয়৷
MVP-এর সাহায্যে, আপনি স্বল্প মেয়াদে এবং সীমিত বাজেটে মৌলিক কার্যকারিতা সহ একটি অ্যাপ তৈরি করতে পারেন, ব্যবহারকারীর প্রতিক্রিয়া সংগ্রহ করতে পারেন এবং এই প্রতিক্রিয়া অনুসারে আপনার উন্নয়ন দলের সাথে সমাধানটি প্রসারিত করতে পারেন।
গেমিং শিল্পে MVP ক্রমশ জনপ্রিয় হয়ে উঠছে। আজ, আমরা ফ্লাটার এবং ফ্লেমের সাথে দ্রুত গেম MVP ডেভেলপমেন্টের ইনস এবং আউটগুলি অন্বেষণ করব, ক্রস-প্ল্যাটফর্ম ন্যূনতম-কার্যকর পণ্য তৈরির জন্য একটি দুর্দান্ত সমন্বয়।
ফ্লাটার, ক্রস-প্ল্যাটফর্ম ডেভেলপমেন্টের জন্য একটি ফিচার-প্যাকড এবং সুরক্ষিত প্ল্যাটফর্ম , মোবাইল অ্যাপ বিশ্বকে ঝড় তুলেছে এবং এর নাগাল UI-এর বাইরেও বিস্তৃত। Flutter- এর উপরে তৈরি একটি শক্তিশালী এবং ওপেন-সোর্স গেম ইঞ্জিন Flame-এর সাহায্যে আপনি অত্যাশ্চর্য 2D গেম তৈরি করতে পারেন যা Android, iOS, ওয়েব এবং ডেস্কটপ ডিভাইসে সহজে চলে।
ফ্লটার গেম এমভিপি তৈরির জন্য একটি জনপ্রিয় সমাধান হয়ে উঠেছে এর অবিচ্ছেদ্য বৈশিষ্ট্যগুলির কারণে যা বিভিন্ন ডিভাইসে মৌলিক কার্যকারিতা উপস্থাপন করে এমন সমাধানগুলির দ্রুত বিকাশকে সহজতর করে। বিশেষ করে, বিভিন্ন ফ্লটার সুবিধা এবং অবিচ্ছেদ্য ফাংশন অনুমতি দেয়:
ফ্লাটার অনেক কম্পিউটিং সংস্থান গ্রহণ করে না এবং ক্রস-প্ল্যাটফর্ম অ্যাপ্লিকেশনগুলির সহজ সেটআপের সুবিধা দেয়।
ফ্লাটার এবং ফ্লেম সংমিশ্রণের উপর ভিত্তি করে এমভিপি অ্যাপটি একটি নির্ভরযোগ্য কিন্তু তুলনামূলকভাবে সহজ সমাধান তৈরি করা। এটি সরাসরি নেটিভ কোডে কম্পাইল করে, মসৃণ গেমপ্লে এবং প্রতিক্রিয়াশীলতা নিশ্চিত করে। আপনি আপনার গেম এমভিপি একবার বিকাশ করতে পারেন এবং এটিকে বিভিন্ন প্ল্যাটফর্মে স্থাপন করতে পারেন, সময় এবং সংস্থান সাশ্রয় করতে পারেন। ফ্লাটার এবং ফ্লেম হুডের নীচে প্ল্যাটফর্মের পার্থক্যগুলি পরিচালনা করে।
উপরন্তু, উভয় প্রযুক্তিই ব্যাপক ডকুমেন্টেশন, টিউটোরিয়াল এবং কোড উদাহরণ সহ প্রাণবন্ত সম্প্রদায়ের গর্ব করে। এর মানে হল যে আপনি কখনই একটি উত্তর বা অনুপ্রেরণার জন্য আটকে থাকবেন না।
ফ্লেম স্বল্প মেয়াদে এবং অতিরিক্ত সম্পদ ব্যয় না করে MVP গেম বৈশিষ্ট্য তৈরি করার জন্য একটি সম্পূর্ণ টুলসেট প্রদান করে। এই ক্রস-প্ল্যাটফর্ম মডেলিং ফ্রেমওয়ার্কটি বিভিন্ন ব্যবহারের ক্ষেত্রে বিস্তৃত অ্যারের জন্য সরঞ্জাম সরবরাহ করে:
উপরে উল্লিখিত বৈশিষ্ট্যগুলির বেশিরভাগই অনেক গেমের জন্য অপরিহার্য এবং এমভিপি বিকাশ পর্যায়েও উপেক্ষা করা উচিত নয়। যা সত্যিই গুরুত্বপূর্ণ তা হল ফ্লেম উপরে উল্লিখিত কার্যকারিতা বিকাশের গতিকে উল্লেখযোগ্যভাবে বৃদ্ধি করে, আপনাকে এমনকি প্রথম দিকের পণ্য সংস্করণেও এই জাতীয় বৈশিষ্ট্যগুলি প্রকাশ করতে দেয়।
এখন, শিখা সম্পর্কে কথা না বলে, এই কাঠামোর সাথে আমাদের নিজস্ব গেমের মৌলিক বৈশিষ্ট্য সম্বলিত একটি MVP তৈরি করা যাক। আমরা শুরু করার আগে, আপনি অবশ্যই Flutter 3.13 বা উচ্চতর, আপনার প্রিয় IDE এবং পরীক্ষার জন্য ডিভাইস ইনস্টল করেছেন।
এই গেমটি ক্রোম ডিনো দ্বারা অনুপ্রাণিত। আহ, বিখ্যাত ডিনো রান! এটি Chrome এর থেকে একটি গেমের চেয়েও বেশি কিছু। এটি ব্রাউজারের অফলাইন মোডের মধ্যে লুকানো একটি প্রিয় ইস্টার ডিম।
আমাদের প্রকল্পে নিম্নলিখিত গেমপ্লে থাকবে:
এবং এটিকে "ফরেস্ট রান" বলা হবে!
একটি খালি ফ্লাটার প্রকল্প তৈরি করুন যেমন আপনি প্রতিবার একটি নতুন অ্যাপ শুরু করার সময় করেন। শুরু করার জন্য, আমাদের প্রকল্পের জন্য pubspec.yaml-এ নির্ভরতা সেট করতে হবে। এই পোস্টটি লেখার সময়, শিখার সর্বশেষ সংস্করণটি হল 1.14.0। এছাড়াও, আসুন এখন সমস্ত সম্পদ পাথ সংজ্ঞায়িত করি, তাই পরে এই ফাইলে ফিরে যাওয়ার প্রয়োজন হবে না। এবং ডিরেক্টরি সম্পদ/images/ এ ছবি রাখুন। আমাদের এটি এখানে রাখতে হবে কারণ শিখা ঠিক এই পথটি স্ক্যান করবে:
environment: sdk: '>=3.2.3 <4.0.0' flutter: '>=3.13.0' dependencies: flutter: sdk: flutter flame: ^1.14.0 flutter: uses-material-design: true assets: - assets/images/ - assets/images/character/ - assets/images/background/ - assets/images/forest/ - assets/images/font/
সমস্ত ছবিকে সম্পদ/ছবি/ এর অধীনে রাখতে মনে রাখবেন কারণ ফ্লেম অন্যান্য ডিরেক্টরি পার্স করবে না।
যেকোন খেলার জন্য আপনার প্রচুর ছবি লাগবে। কিন্তু আপনি যদি ডিজাইনে ভালো না হন? সৌভাগ্যক্রমে, প্রচুর ওপেন-সোর্স সম্পদ রয়েছে যা আপনি আপনার প্রকল্পগুলির জন্য ব্যবহার করতে পারেন। এই গেমের জন্য সম্পদ itch.io থেকে নেওয়া হয়েছে। আমরা আমাদের প্রকল্পের জন্য এই সংস্থানগুলি ব্যবহার করব:
আপনি সেই লিঙ্কগুলিতে যেতে পারেন, অথবা এই প্রকল্পের জন্য প্রস্তুত সম্পদ (সম্পদ সংরক্ষণাগারের লিঙ্ক) ডাউনলোড করুন এবং আপনার প্রকল্পে সমস্ত সামগ্রী অনুলিপি করুন৷
ফ্লাটারের সাথে শিখার অনুরূপ দর্শন রয়েছে। ফ্লটারে, সবকিছুই একটি উইজেট; শিখায়, সবকিছুই একটি উপাদান, এমনকি পুরো খেলা। প্রতিটি উপাদান 2টি পদ্ধতি ওভাররাইড করতে পারে: onLoad() এবং update()। onLoad() শুধুমাত্র একবার কল করা হয় যখন ComponentTree এ কম্পোনেন্ট মাউন্ট করা হয় এবং প্রতিটি ফ্রেমে আপডেট() ফায়ার করা হয়। Flutter-এ StatefulWidget থেকে initState() এবং build() এর সাথে খুব মিল।
এখন, কিছু কোড লিখি. একটি ক্লাস তৈরি করুন যা FlameGame প্রসারিত করে এবং আমাদের সমস্ত সম্পদ ক্যাশে লোড করে।
class ForestRunGame extends FlameGame { @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); } }
এরপর, main.dart-এ ForestRunGame ব্যবহার করুন। এছাড়াও, আপনি ডিভাইসের অভিযোজন কনফিগার করতে Flame.device থেকে পদ্ধতি ব্যবহার করতে পারেন। এবং গেমউইজেট রয়েছে, যা উইজেট এবং উপাদানগুলির মধ্যে সেতু হিসাবে কাজ করে।
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.device.fullScreen(); await Flame.device.setLandscape(); runApp(GameWidget(game: ForestRunGame())); }
এই মুহুর্তে, আমরা ইতিমধ্যে গেমটি শুরু করতে পারি, তবে কেবল একটি কালো পর্দা থাকবে। সুতরাং, আমাদের উপাদান যোগ করতে হবে।
আমরা বনকে দুটি উপাদানে ভাগ করব: পটভূমি এবং অগ্রভাগ। প্রথমত, আমরা ব্যাকগ্রাউন্ড পরিচালনা করব। আপনি কি কখনো গতিশীল অনুভূত একটি পৃষ্ঠার মাধ্যমে স্ক্রোল করেছেন? যেন আপনি একবারে একাধিক ভিউ স্ক্রোল করছেন? এটি একটি প্যারালাক্স প্রভাব, এবং এটি ঘটে যখন একটি পৃষ্ঠার বিভিন্ন উপাদান বিভিন্ন গতিতে চলে, একটি 3D গভীরতা প্রভাব তৈরি করে।
আপনি ভাবতে পারেন, আমরা আমাদের পটভূমির জন্য একটি প্যারালাক্স ব্যবহার করব। ParallaxComponent প্রসারিত করুন এবং ParallaxImageData ব্যবহার করে চিত্রগুলির একটি স্ট্যাক সেট আপ করুন। এছাড়াও, প্রাথমিক স্তরগুলির গতি এবং বেগের মাল্টিপ্লিয়ার ডেল্টার জন্য বেসভেলোসিটি রয়েছে, যা স্তরগুলির মধ্যে গতির আপেক্ষিক পার্থক্যকে বোঝায়। এবং শেষ জিনিস, অগ্রাধিকার ক্ষেত্রটি কনফিগার করুন (z-index) এটিকে অন্যান্য উপাদানগুলির পিছনে সরাতে।
class ForestBackground extends ParallaxComponent<ForestRunGame> { @override Future<void> onLoad() async { priority = -10; parallax = await game.loadParallax( [ ParallaxImageData('background/plx-1.png'), ParallaxImageData('background/plx-2.png'), ParallaxImageData('background/plx-3.png'), ParallaxImageData('background/plx-4.png'), ParallaxImageData('background/plx-5.png'), ], baseVelocity: Vector2.zero(), velocityMultiplierDelta: Vector2(1.4, 1.0), ); } }
পটভূমি সম্পন্ন হয়; এখন, ফোরগ্রাউন্ড যোগ করার সময়। PositionComponent প্রসারিত করুন যাতে আমরা স্ক্রীনের নীচে স্থলটি সারিবদ্ধ করতে পারি। গেম ক্যাশে অ্যাক্সেস করার জন্য আমাদের HasGameReference মিক্সিনও প্রয়োজন।
গ্রাউন্ড তৈরি করতে, আপনাকে গ্রাউন্ড ব্লক ইমেজটিকে একাধিকবার লাইনে রাখতে হবে। শিখায়, চিত্রের উপাদানগুলিকে বলা হয় স্প্রাইট। একটি স্প্রাইট হল একটি চিত্রের একটি অঞ্চল যা ক্যানভাসে রেন্ডার করা যেতে পারে। এটি সম্পূর্ণ চিত্রের প্রতিনিধিত্ব করতে পারে বা একটি স্প্রাইট শীট গঠিত টুকরাগুলির মধ্যে একটি হতে পারে।
এছাড়াও, মনে রাখবেন যে X-অক্ষটি ডান-ভিত্তিক এবং Y-অক্ষটি নীচে-ভিত্তিক। অক্ষগুলির কেন্দ্রটি স্ক্রিনের বাম উপরের কোণায় অবস্থিত।
class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { static final blockSize = Vector2(480, 96); late final Sprite groundBlock; late final Queue<SpriteComponent> ground; @override void onLoad() { super.onLoad(); groundBlock = Sprite(game.images.fromCache('forest/ground.png')); ground = Queue(); } @override void onGameResize(Vector2 size) { super.onGameResize(size); final newBlocks = _generateBlocks(); ground.addAll(newBlocks); addAll(newBlocks); y = size.y - blockSize.y; } List<SpriteComponent> _generateBlocks() { final number = 1 + (game.size.x / blockSize.x).ceil() - ground.length; final lastBlock = ground.lastOrNull; final lastX = lastBlock == null ? 0 : lastBlock.x + lastBlock.width; return List.generate( max(number, 0), (i) => SpriteComponent( sprite: groundBlock, size: blockSize, position: Vector2(lastX + blockSize.x * i, y), priority: -5, ), growable: false, ); } }
এবং শেষ জিনিস, এই উপাদানগুলি আমাদের ForestRunGame এ যোগ করুন।
class ForestRunGame extends FlameGame { late final foreground = ForestForeground(); late final background = ForestBackground(); @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); add(foreground); add(background); } }
এখন, গেমটি চালু করার চেষ্টা করুন। এই মুহুর্তে আমরা ইতিমধ্যে আমাদের বন আছে.
বন দেখতে ভাল, কিন্তু এই মুহূর্তে এটি শুধুমাত্র একটি ছবি. সুতরাং, আমরা জ্যাক তৈরি করতে যাচ্ছি, যে খেলোয়াড়ের নির্দেশনায় এই বনের মধ্য দিয়ে দৌড়াবে। গাছ এবং মাটির বিপরীতে, খেলোয়াড়ের জীবিত অনুভব করার জন্য অ্যানিমেশন প্রয়োজন। আমরা গ্রাউন্ড ব্লকের জন্য স্প্রাইট ব্যবহার করেছি, কিন্তু আমরা জ্যাকের জন্য স্প্রাইট অ্যানিমেশন ব্যবহার করতে যাচ্ছি। কিভাবে কাজ করে? ওয়েল, সব সহজ, আপনি শুধু sprites একটি ক্রম লুপ প্রয়োজন. উদাহরণস্বরূপ, আমাদের রান অ্যানিমেশনে 8টি স্প্রাইট রয়েছে, যা একটি ছোট সময়ের ব্যবধানে একে অপরকে প্রতিস্থাপন করে।
জ্যাক দৌড়াতে, লাফ দিতে এবং নিষ্ক্রিয় হতে পারে। তার রাজ্যের প্রতিনিধিত্ব করতে, আমরা একটি প্লেয়ারস্টেট enum যোগ করতে পারি। তারপরে একটি প্লেয়ার তৈরি করুন যা SpriteAnimationGroupComponent প্রসারিত করে এবং PlayerStateকে জেনেরিক আর্গুমেন্ট হিসাবে পাস করে। এই উপাদানটির একটি অ্যানিমেশন ক্ষেত্র রয়েছে যেখানে প্রতিটি প্লেয়ারস্টেটের জন্য অ্যানিমেশনগুলি সংরক্ষণ করা হয় এবং একটি বর্তমান ক্ষেত্র, যা প্লেয়ারের বর্তমান অবস্থার প্রতিনিধিত্ব করে, যা অ্যানিমেটেড করা প্রয়োজন৷
enum PlayerState { jumping, running, idle } class Player extends SpriteAnimationGroupComponent<PlayerState> { @override void onLoad() { super.onLoad(); animations = { PlayerState.running: SpriteAnimation.fromFrameData( game.images.fromCache('character/run.png'), SpriteAnimationData.sequenced( amount: 8, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(23, 34), ), ), PlayerState.idle: SpriteAnimation.fromFrameData( game.images.fromCache('character/idle.png'), SpriteAnimationData.sequenced( amount: 12, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(21, 35), ), ), PlayerState.jumping: SpriteAnimation.spriteList( [ Sprite(game.images.fromCache('character/jump.png')), Sprite(game.images.fromCache('character/land.png')), ], stepTime: 0.4, loop: false, ), }; current = PlayerState.idle; } }
প্লেয়ার স্টেট প্রস্তুত। এখন, আমাদের প্লেয়ারটিকে পর্দায় একটি আকার এবং অবস্থান দিতে হবে। আমি তার আকার 69x102 পিক্সেল সেট করতে যাচ্ছি, কিন্তু আপনার পছন্দ মত এটি পরিবর্তন করতে দ্বিধা বোধ করবেন না। অবস্থানের জন্য, আমাদের অবশ্যই স্থলের স্থানাঙ্কগুলি জানতে হবে। HasGameReference মিক্সিন যোগ করে, আমরা ফোরগ্রাউন্ড ফিল্ড অ্যাক্সেস করতে পারি এবং এর স্থানাঙ্ক পেতে পারি। এখন, আসুন onGameResize পদ্ধতিটিকে ওভাররাইড করি, যা প্রতিবার অ্যাপ্লিকেশনের আকার পরিবর্তন করার সময় বলা হয় এবং সেখানে জ্যাকের অবস্থান সেট করুন।
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { static const startXPosition = 80.0; Player() : super(size: Vector2(69, 102)); double get groundYPosition => game.foreground.y - height + 20; // onLoad() {...} with animation setup @override void onGameResize(Vector2 size) { super.onGameResize(size); x = startXPosition; y = groundYPosition; } }
আগে যেমন করা হয়েছিল, আমাদের গেমে প্লেয়ার যোগ করুন।
class ForestRunGame extends FlameGame { // Earlier written code here... late final player = Player(); @override Future<void> onLoad() async { // Earlier written code here... add(player); } }
আপনি যদি গেমটি শুরু করেন, আপনি দেখতে পাবেন জ্যাক ইতিমধ্যে বনে রয়েছে!
আমাদের গেমের তিনটি স্টেট আছে: ইন্ট্রো, প্লে এবং গেম ওভার। সুতরাং, আমরা enum GameState যোগ করব যা সেইগুলির প্রতিনিধিত্ব করে। জ্যাক চালানোর জন্য, আমাদের গতি এবং ত্বরণ ভেরিয়েবল প্রয়োজন। এছাড়াও, আমাদের ভ্রমণ করা দূরত্ব গণনা করতে হবে (পরে ব্যবহার করা হবে)।
আগেই উল্লেখ করা হয়েছে, কম্পোনেন্টের দুটি প্রধান পদ্ধতি রয়েছে: onLoad() এবং update()। আমরা ইতিমধ্যে কয়েকবার অনলোড পদ্ধতি ব্যবহার করেছি। এখন, আপডেট() নিয়ে কথা বলি। এই পদ্ধতিতে dt নামে একটি প্যারামিটার রয়েছে। এটি সেই সময়ের প্রতিনিধিত্ব করে যা শেষবার আপডেট() কল করা হয়েছিল।
বর্তমান গতি এবং ভ্রমণের দূরত্ব গণনা করতে, আমরা আপডেট() পদ্ধতি এবং কিছু মৌলিক গতিবিদ্যা সূত্র ব্যবহার করব:
enum GameState { intro, playing, gameOver } class ForestRunGame extends FlameGame { static const acceleration = 10.0; static const maxSpeed = 2000.0; static const startSpeed = 400.0; GameState state = GameState.intro; double currentSpeed = 0; double traveledDistance = 0; // Earlier written code here... @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } }
প্রকৃতপক্ষে, আমরা উন্নয়নকে সহজ করার জন্য একটি কৌশল ব্যবহার করব: জ্যাক স্থির থাকবে, কিন্তু বন জ্যাকের দিকে এগিয়ে যাবে। সুতরাং, খেলার গতি প্রয়োগ করার জন্য আমাদের বন প্রয়োজন।
প্যারালাক্স ব্যাকগ্রাউন্ডের জন্য, আমাদের শুধু গেমের গতি পাস করতে হবে। এবং এটি স্বয়ংক্রিয়ভাবে বাকিগুলি পরিচালনা করবে।
class ForestBackground extends ParallaxComponent<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); parallax?.baseVelocity = Vector2(game.currentSpeed / 10, 0); } }
অগ্রভাগের জন্য, আমাদের প্রতিটি গ্রাউন্ড ব্লককে স্থানান্তর করতে হবে। এছাড়াও, সারির প্রথম ব্লকটি স্ক্রীন ছেড়ে গেছে কিনা তা আমাদের পরীক্ষা করতে হবে। যদি তাই হয়, তাহলে এটি সরিয়ে ফেলুন এবং সারির শেষে রাখুন;
class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); final shift = game.currentSpeed * dt; for (final block in ground) { block.x -= shift; } final firstBlock = ground.first; if (firstBlock.x <= -firstBlock.width) { firstBlock.x = ground.last.x + ground.last.width; ground.remove(firstBlock); ground.add(firstBlock); } } }
সবকিছু প্রস্তুত, কিন্তু একটি ট্রিগার. আমরা ক্লিকে চালানো শুরু করতে চাই। আমাদের লক্ষ্য মোবাইল এবং ডেস্কটপ উভয়ই, তাই আমরা স্ক্রিন ট্যাপ এবং কীবোর্ড ইভেন্টগুলি পরিচালনা করতে চাই।
ভাগ্যক্রমে, শিখা এটি করার একটি উপায় আছে। শুধু আপনার ইনপুট ধরনের জন্য একটি মিশ্রণ যোগ করুন. কীবোর্ডের জন্য, এটি স্ক্রিন ট্যাপ করার জন্য কীবোর্ড ইভেন্ট এবং ট্যাপক্যালব্যাক। এই মিশ্রণগুলি আপনাকে সম্পর্কিত পদ্ধতিগুলিকে ওভাররাইড করার এবং আপনার যুক্তি প্রদান করার সম্ভাবনা দেয়।
ব্যবহারকারী স্পেসবার টিপে বা স্ক্রীনে ট্যাপ করলে গেমটি অবশ্যই শুরু হবে।
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { start(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { start(); } void start() { state = GameState.playing; player.current = PlayerState.running; currentSpeed = startSpeed; traveledDistance = 0; } }
ফলে ক্লিক করার পর এখন জ্যাক রান করতে পারবেন।
এখন, আমরা রাস্তায় প্রতিবন্ধকতা থাকতে চাই। আমাদের ক্ষেত্রে, তারা বিষাক্ত ঝোপ হিসাবে প্রতিনিধিত্ব করা হবে। বুশ অ্যানিমেটেড নয়, তাই আমরা স্প্রাইট কম্পোনেন্ট ব্যবহার করতে পারি। এছাড়াও, এর গতি অ্যাক্সেস করার জন্য আমাদের একটি গেমের রেফারেন্স প্রয়োজন। আর একটা কথা; আমরা একের পর এক ঝোপ তৈরি করতে চাই না, কারণ এই পদ্ধতিটি এমন পরিস্থিতির সৃষ্টি করতে পারে যখন জ্যাক কেবল একটি লাফ দিয়ে ঝোপের একটি লাইন অতিক্রম করতে পারে না। এটি পরিসীমা থেকে একটি এলোমেলো সংখ্যা, যা বর্তমান গেমের গতির উপর নির্ভর করে।
class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { late double gap; Bush() : super(size: Vector2(200, 84)); bool get isVisible => x + width > 0; @override Future<void> onLoad() async { x = game.size.x + width; y = -height + 20; gap = _computeRandomGap(); sprite = Sprite(game.images.fromCache('forest/bush.png')); } double _computeRandomGap() { final minGap = width * game.currentSpeed * 100; final maxGap = minGap * 5; return (Random().nextDouble() * (maxGap - minGap + 1)).floor() + minGap; } @override void update(double dt) { super.update(dt); x -= game.currentSpeed * dt; if (!isVisible) { removeFromParent(); } } }
কে ঝোপ রোপণ করছে? প্রকৃতি, অবশ্যই। আসুন প্রকৃতি তৈরি করি যা আমাদের বুশ প্রজন্মকে পরিচালনা করবে।
class Nature extends Component with HasGameReference<ForestRunGame> { @override void update(double dt) { super.update(dt); if (game.currentSpeed > 0) { final plant = children.query<Bush>().lastOrNull; if (plant == null || (plant.x + plant.width + plant.gap) < game.size.x) { add(Bush()); } } } }
এখন, আমাদের বনভূমিতে প্রকৃতি যোগ করা যাক।
class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... late final Nature nature; @override void onLoad() { // Earlier written code here... nature = Nature(); add(nature); }
এখন আমাদের বনে ঝোপঝাড় আছে। কিন্তু অপেক্ষা করুন, জ্যাক তাদের মধ্য দিয়ে চলছে। ইহা কি জন্য ঘটিতেছে? এর কারণ আমরা এখনও আঘাত প্রয়োগ করিনি।
এখানে, হিটবক্স আমাদের সাহায্য করবে। হিটবক্স হল ফ্লেমের চিড়িয়াখানার উপাদানগুলির আরেকটি উপাদান। এটি সংঘর্ষ সনাক্তকরণকে এনক্যাপসুলেট করে এবং আপনাকে কাস্টম যুক্তি দিয়ে এটি পরিচালনা করার সম্ভাবনা দেয়।
জ্যাকের জন্য একটি যোগ করুন। মনে রাখবেন যে উপাদানটির অবস্থান তার বাম-ডান কোণে স্থাপন করবে, কেন্দ্রে নয়। এবং আকারের সাথে, আপনি বাকিগুলি পরিচালনা করেন।
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(2, 2), size: Vector2(60, 100), ), ); } }
এবং গুল্ম জন্য একটি. এখানে, কিছু অপ্টিমাইজেশানের জন্য আমরা সংঘর্ষের ধরনটিকে প্যাসিভ এ সেট করব। ডিফল্টরূপে, টাইপটি সক্রিয়, যার মানে হল যে এই হিটবক্সটি অন্য প্রতিটি হিটবক্সের সাথে সংঘর্ষ হয়েছে কিনা তা শিখবে। আমরা শুধুমাত্র একটি প্লেয়ার এবং ঝোপ আছে. যেহেতু প্লেয়ারের ইতিমধ্যেই একটি সক্রিয় সংঘর্ষের ধরন রয়েছে এবং ঝোপগুলি একে অপরের সাথে সংঘর্ষ করতে পারে না, তাই আমরা টাইপটিকে প্যাসিভ হিসাবে সেট করতে পারি।
class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(30, 30), size: Vector2(150, 54), collisionType: CollisionType.passive, ), ); } }
এটি দুর্দান্ত, কিন্তু হিটবক্সের অবস্থান ঠিকভাবে সামঞ্জস্য করা হয়েছে কিনা তা আমি দেখতে পাচ্ছি না। আমি কিভাবে এটা পরীক্ষা করতে পারেন?
ঠিক আছে, আপনি প্লেয়ার এবং বুশের ডিবাগমোড ক্ষেত্রটিকে সত্যে সেট করতে পারেন। এটি আপনাকে আপনার হিটবক্সগুলি কীভাবে অবস্থান করছে তা দেখতে অনুমতি দেবে। বেগুনি উপাদানের আকার নির্ধারণ করে এবং হলুদ হিটবক্স নির্দেশ করে।
এখন, আমরা সনাক্ত করতে চাই যখন প্লেয়ার এবং বুশের মধ্যে সংঘর্ষ হয়। এর জন্য, আপনাকে গেমে HasCollisionDetection মিক্সিন যোগ করতে হবে এবং তারপরে উপাদানগুলির জন্য CollisionCallbacks যোগ করতে হবে, যা সংঘর্ষ পরিচালনা করতে হবে।
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... }
আপাতত, সংঘর্ষ শনাক্ত হলে গেমটি বিরতি দিন।
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.paused = true; } }
জ্যাক যদি সেই ঝোপ এড়াতে চায়, তবে তাকে লাফ দিতে হবে। আসুন তাকে শেখাই। এই বৈশিষ্ট্যের জন্য, আমাদের প্রয়োজন মাধ্যাকর্ষণ ধ্রুবক এবং জ্যাকের লাফের প্রাথমিক উল্লম্ব গতি। সেই মানগুলি চোখের দ্বারা বেছে নেওয়া হয়েছিল, তাই সেগুলিকে সামঞ্জস্য করতে নির্দ্বিধায়৷
তাহলে, মহাকর্ষ কিভাবে কাজ করে? মূলত, এটি একই ত্বরণ কিন্তু মাটির দিকে ভিত্তিক। সুতরাং, আমরা উল্লম্ব অবস্থান এবং গতির জন্য একই সূত্র ব্যবহার করতে পারি। সুতরাং, আমাদের লাফের 3টি ধাপ থাকবে:
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { static const gravity = 1400.0; static const initialJumpVelocity = -700.0; double jumpSpeed = 0; // Earlier written code here... void jump() { if (current != PlayerState.jumping) { current = PlayerState.jumping; jumpSpeed = initialJumpVelocity - (game.currentSpeed / 500); } } void reset() { y = groundYPos; jumpSpeed = 0; current = PlayerState.running; } @override void update(double dt) { super.update(dt); if (current == PlayerState.jumping) { y += jumpSpeed * dt; jumpSpeed += gravity * dt; if (y > groundYPos) { reset(); } } else { y = groundYPos; } } }
এবং এখন ForestRunGame থেকে ক্লিক করে জাম্পিং ট্রিগার করি
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { onAction(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { onAction(); } void onAction() { switch (state) { case GameState.intro: case GameState.gameOver: start(); break; case GameState.playing: player.jump(); break; } } }
এখন, জ্যাক ঝোপ সামলাতে পারে।
খেলা শেষ হলে, আমরা স্ক্রিনে পাঠ্য দেখাতে চাই। ফ্লাটার থেকে টেক্সট ইন ফ্ল্যাম ভিন্নভাবে কাজ করে। আপনাকে প্রথমে একটি ফন্ট তৈরি করতে হবে। হুডের নিচে, এটি শুধু একটি মানচিত্র, যেখানে চর একটি কী এবং স্প্রাইট একটি মান। প্রায় সবসময়, গেমের ফন্ট হল একটি চিত্র যেখানে সমস্ত প্রয়োজনীয় চিহ্ন একত্রিত হয়।
এই খেলার জন্য, আমাদের শুধুমাত্র সংখ্যা এবং ক্যাপ অক্ষর প্রয়োজন। তো, আসুন আমাদের ফন্ট তৈরি করি। এটি করার জন্য, আপনাকে উত্স চিত্র এবং গ্লিফ পাস করতে হবে। একটি গ্লিফ কি? Glyph হল চর, এর আকার এবং উৎস চিত্রের অবস্থান সম্পর্কে তথ্যের মিলন।
class StoneText extends TextBoxComponent { static const digits = '123456789'; static const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; StoneText({ required Image source, required super.position, super.text = '', }) : super( textRenderer: SpriteFontRenderer.fromFont( SpriteFont( source: source, size: 32, ascent: 32, glyphs: [ _buildGlyph(char: '0', left: 480, top: 0), for (var i = 0; i < digits.length; i++) _buildGlyph(char: digits[i], left: 32.0 * i, top: 32), for (var i = 0; i < letters.length; i++) _buildGlyph( char: letters[i], left: 32.0 * (i % 16), top: 64.0 + 32 * (i ~/ 16), ), ], ), letterSpacing: 2, ), ); static Glyph _buildGlyph({ required String char, required double left, required double top, }) => Glyph(char, left: left, top: top, height: 32, width: 32); }
এখন, আমরা প্যানেলে গেমটি তৈরি করতে পারি এবং গেমটিতে এটি ব্যবহার করতে পারি।
class GameOverPanel extends PositionComponent with HasGameReference<ForestRunGame> { bool visible = false; @override Future<void> onLoad() async { final source = game.images.fromCache('font/keypound.png'); add(StoneText(text: 'GAME', source: source, position: Vector2(-144, -16))); add(StoneText(text: 'OVER', source: source, position: Vector2(16, -16))); } @override void renderTree(Canvas canvas) { if (visible) { super.renderTree(canvas); } } @override void onGameResize(Vector2 size) { super.onGameResize(size); x = size.x / 2; y = size.y / 2; } }
এখন, আমরা আমাদের প্যানেল দেখাতে পারি, যখন জ্যাক ঝোপে আঘাত করে। এছাড়াও আসুন start() পদ্ধতিটি পরিবর্তন করি, যাতে আমরা ক্লিকে গেমটি পুনরায় চালু করতে পারি। এছাড়াও, আমাদের বন থেকে সমস্ত ঝোপ পরিষ্কার করতে হবে।
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... late final gameOverPanel = GameOverPanel(); @override Future<void> onLoad() async { // Earlier written code here... add(gameOverPanel); } void gameOver() { paused = true; gameOverPanel.visible = true; state = GameState.gameOver; currentSpeed = 0; } void start() { paused = false; state = GameState.playing; currentSpeed = startSpeed; traveledDistance = 0; player.reset(); foreground.nature.removeAll(foreground.nature.children); gameOverPanel.visible = false; } }
এবং এখন, আমাদের প্লেয়ারে সংঘর্ষ কলব্যাক আপডেট করতে হবে।
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.gameOver(); } }
এখন, আপনি গেম ওভার দেখতে পারেন যখন জ্যাক একটি ঝোপে আঘাত করে। এবং আবার ক্লিক করে গেমটি পুনরায় চালু করুন।
এবং চূড়ান্ত স্পর্শ-স্কোর গণনা.
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { late final StoneText scoreText; late final StoneText highText; late final StoneText highScoreText; int _score = 0; int _highScore = 0; // Earlier written code here... @override Future<void> onLoad() async { // Earlier written code here... final font = images.fromCache('font/keypound.png'); scoreText = StoneText(source: font, position: Vector2(20, 20)); highText = StoneText(text: 'HI', source: font, position: Vector2(256, 20)); highScoreText = StoneText( text: '00000', source: font, position: Vector2(332, 20), ); add(scoreText); add(highScoreText); add(highText); setScore(0); } void start() { // Earlier written code here... if (_score > _highScore) { _highScore = _score; highScoreText.text = _highScore.toString().padLeft(5, '0'); } _score = 0; } @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; setScore(traveledDistance ~/ 50); if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } void setScore(int score) { _score = score; scoreText.text = _score.toString().padLeft(5, '0'); } }
যে সব লোকেরা!
এখন, এটি চেষ্টা করে দেখুন, এবং আমার উচ্চ স্কোর বীট করার চেষ্টা করুন। এটা 2537 পয়েন্ট!
এটা অনেক ছিল, কিন্তু আমরা এটা করেছি. আমরা পদার্থবিদ্যা, অ্যানিমেশন, স্কোর গণনা এবং আরও অনেক কিছু সহ একটি মোবাইল গেমের জন্য একটি ন্যূনতম কার্যকর পণ্য তৈরি করেছি। উন্নতির জন্য সবসময় জায়গা থাকে এবং, অন্য যেকোন MVP এর মতই, আমাদের পণ্যটি ভবিষ্যতে নতুন বৈশিষ্ট্য, মেকানিক্স এবং গেম মোড সহ আশা করা যেতে পারে।
এছাড়াও, একটি flame_audio প্যাকেজ রয়েছে, যা আপনি কিছু ব্যাকগ্রাউন্ড মিউজিক, লাফানো বা আঘাত করার শব্দ ইত্যাদি যোগ করতে ব্যবহার করতে পারেন।
আপাতত, আমাদের মূল উদ্দেশ্য ছিল স্বল্প মেয়াদে এবং সীমিত সম্পদ বরাদ্দ দিয়ে মৌলিক পণ্য কার্যকারিতা তৈরি করা। Flutter এবং Flame এর সমন্বয় একটি গেম MVP তৈরির জন্য উপযুক্ত বলে প্রমাণিত হয়েছে যা ব্যবহারকারীর প্রতিক্রিয়া সংগ্রহ করতে এবং ভবিষ্যতে অ্যাপটিকে আপগ্রেড করতে ব্যবহার করা যেতে পারে।
আপনি এখানে আমাদের প্রচেষ্টার ফলাফল পরীক্ষা করতে পারেন.
এর শক্তিশালী বৈশিষ্ট্য, ব্যবহারের সহজতা এবং সমৃদ্ধিশীল সম্প্রদায়ের সাথে, ফ্লাটার এবং ফ্লেম উচ্চাকাঙ্ক্ষী গেম বিকাশকারীদের জন্য একটি বাধ্যতামূলক পছন্দ। আপনি একজন অভিজ্ঞ পেশাদার হন বা সবেমাত্র শুরু করেন, এই সংমিশ্রণটি আপনার গেমের ধারণাগুলিকে জীবন্ত করার জন্য সরঞ্জাম এবং সম্ভাবনা সরবরাহ করে। সুতরাং, আপনার সৃজনশীলতা ধরুন, ফ্লটার এবং ফ্লেমের জগতে ডুব দিন এবং পরবর্তী মোবাইল গেমিং সংবেদন তৈরি করা শুরু করুন!
আমরা আশা করি আপনি এই নিবন্ধটি উপভোগ্য এবং তথ্যপূর্ণ পেয়েছেন। আপনি যদি সফ্টওয়্যার বিকাশের বিষয়ে আরও অন্তর্দৃষ্টি চান বা আপনার নিজের MVP প্রকল্প নিয়ে আলোচনা করতে চান তবে লিওবিট অন্বেষণ করতে বা আমাদের প্রযুক্তিগত দলের সাথে যোগাযোগ করতে দ্বিধা করবেন না!