मैं हमेशा वीडियो गेम बनाना चाहता हूं। मेरा पहला एंड्रॉइड ऐप जिसने मुझे मेरी पहली नौकरी पाने में मदद की, वह एक साधारण गेम था, जिसे एंड्रॉइड व्यूज के साथ बनाया गया था। उसके बाद, गेम इंजन का उपयोग करके अधिक विस्तृत गेम बनाने के कई प्रयास हुए, लेकिन समय की कमी या ढांचे की जटिलता के कारण वे सभी विफल रहे। लेकिन जब मैंने पहली बार फ्लटर पर आधारित फ्लेम इंजन के बारे में सुना, तो मैं इसकी सादगी और क्रॉस-प्लेटफॉर्म सपोर्ट से तुरंत आकर्षित हुआ, इसलिए मैंने इसके साथ एक गेम बनाने की कोशिश करने का फैसला किया।
मैं इंजन का अनुभव प्राप्त करने के लिए कुछ सरल, लेकिन अभी भी चुनौतीपूर्ण के साथ शुरू करना चाहता था। लेखों की यह श्रृंखला फ्लेम (और स्पंदन) सीखने और एक बुनियादी प्लेटफ़ॉर्मर गेम बनाने की मेरी यात्रा है। मैं इसे काफी विस्तृत बनाने की कोशिश करूँगा, इसलिए यह किसी के लिए भी उपयोगी होना चाहिए जो सामान्य रूप से फ्लेम या गेम देव में अपने पैर की उंगलियों को डुबो रहा है।
4 लेखों के दौरान, मैं एक 2डी साइड-स्क्रॉलिंग गेम बनाने जा रहा हूं, जिसमें शामिल हैं:
एक ऐसा पात्र जो दौड़ सकता है और कूद सकता है
एक कैमरा जो खिलाड़ी का अनुसरण करता है
स्क्रॉल स्तर का नक्शा, जमीन और प्लेटफार्मों के साथ
लंबन पृष्ठभूमि
सिक्के जो खिलाड़ी एकत्र कर सकता है और 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 द्वारा आर्केड प्लेटफॉर्मर एसेट्स पैक का उपयोग करूंगा।
आम तौर पर, इमेज एसेट दो रूपों में आते हैं: स्प्राइट शीट और सिंगल स्प्राइट। पूर्व एक बड़ी छवि है, जिसमें सभी खेल संपत्तियां एक में हैं। फिर गेम डेवलपर आवश्यक स्प्राइट की सटीक स्थिति निर्दिष्ट करते हैं, और गेम इंजन इसे शीट से काट देता है। इस गेम के लिए, मैं सिंगल स्प्राइट्स का उपयोग करूंगा (एनिमेशन को छोड़कर, उन्हें एक छवि के रूप में रखना आसान है) क्योंकि मुझे स्प्राइट शीट में प्रदान की गई सभी संपत्तियों की आवश्यकता नहीं है।
चाहे आप स्वयं स्प्राइट्स बना रहे हों या उन्हें किसी कलाकार से प्राप्त कर रहे हों, आपको गेम इंजन के लिए उन्हें अधिक उपयुक्त बनाने के लिए उन्हें स्लाइस करने की आवश्यकता हो सकती है। आप उस उद्देश्य के लिए विशेष रूप से बनाए गए टूल (जैसे टेक्सचर पैकर) या किसी ग्राफिकल एडिटर का उपयोग कर सकते हैं। मैंने Adobe Photoshop का उपयोग किया, क्योंकि इस स्प्राइट शीट में, स्प्राइट्स के बीच असमान स्थान होता है, जिससे छवियों को निकालने के लिए स्वचालित टूल के लिए यह कठिन हो जाता है, इसलिए मुझे इसे मैन्युअल रूप से करना पड़ा।
आप संपत्ति का आकार भी बढ़ाना चाह सकते हैं, लेकिन यदि यह सदिश छवि नहीं है, तो परिणामी स्प्राइट धुंधली हो सकती है। एक वर्कअराउंड मैंने पाया कि पिक्सेल कला के लिए बहुत अच्छा काम करता है, फ़ोटोशॉप में Nearest Neighbour (hard edges)
आकार बदलने की विधि का उपयोग करना है (या Gimp में कोई भी इंटरपोलेशन सेट नहीं है)। लेकिन यदि आपका एसेट अधिक विस्तृत है, तो यह संभवत: काम नहीं करेगा।
रास्ते से बाहर स्पष्टीकरण के साथ, मेरे द्वारा तैयार की गई संपत्ति को डाउनलोड करें या अपनी खुद की तैयार करें और उन्हें अपनी परियोजना के 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
विस्तार करता है, जो कि Flame इंजन में उपयोग की जाने वाली बेस गेम क्लास है। जो बदले में 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
विधि में हम THE_BOY
स्प्राइट शीट से एक नया SpriteAnimation
बनाते हैं जिसे हमने assets.dart
फ़ाइल में घोषित किया था।
अब चलो हमारे खिलाड़ी को खेल में जोड़ें! game.dart
फ़ाइल पर वापस लौटें और onLoad
विधि के निचले भाग में निम्नलिखित जोड़ें:
final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy);
यदि आप अभी खेल चलाते हैं, तो हमें द बॉय से मिलने में सक्षम होना चाहिए!
सबसे पहले, हमें कीबोर्ड से द बॉय को नियंत्रित करने की क्षमता जोड़ने की जरूरत है। चलिए HasKeyboardHandlerComponents
को game.dart
फ़ाइल में मिलाते हैं।
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 पर समान रूप से चलेगा।
ठीक है, अगर हम अभी खेल चलाते हैं, तो हमें यह देखना चाहिए:
बहुत बढ़िया, अब लड़के को बायीं ओर घुमाते हैं। इसे 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
विधि के निचले भाग में लागू करें और खेल को चलाएं।
यह पहले भाग के लिए है। हमने सीखा कि फ्लेम गेम को कैसे सेट अप किया जाए, एसेट्स को कहां खोजा जाए, उन्हें अपने गेम में कैसे लोड किया जाए, और कैसे एक शानदार एनिमेटेड कैरेक्टर बनाया जाए और कीबोर्ड इनपुट के आधार पर इसे स्थानांतरित किया जाए। इस भाग का कोड मेरे जीथब पर पाया जा सकता है।
अगले लेख में, मैं कवर करूंगा कि टाइल का उपयोग करके गेम स्तर कैसे बनाया जाए, फ्लेम कैमरा को कैसे नियंत्रित किया जाए और एक लंबन पृष्ठभूमि को कैसे जोड़ा जाए। बने रहें!
प्रत्येक भाग के अंत में, मैं उन बेहतरीन क्रिएटर्स और संसाधनों की सूची जोड़ूंगा जिनसे मैंने सीखा है।