Yakın zamanda yapılan bir ankete göre her 5 startup'tan yalnızca 2'si kârlı. Bir MVP (minimum uygulanabilir ürün), bir startup'ın karlılık şansını önemli ölçüde artırır çünkü bu tür işletmelerin bütçenin tamamını tam işlevselliğe sahip bir uygulamaya harcamadan erken kullanıcı geri bildirimi toplamasına olanak tanır.
MVP ile kısa vadede ve sınırlı bir bütçeyle temel işlevlere sahip bir uygulama geliştirebilir, kullanıcı geri bildirimleri toplayabilir ve bu geri bildirimlere göre geliştirme ekibinizle birlikte çözümü genişletmeye devam edebilirsiniz.
MVP'ler oyun endüstrisinde giderek daha popüler hale geliyor. Bugün, platformlar arası minimum düzeyde uygulanabilir ürünler oluşturmak için mükemmel bir kombinasyon olan Flutter ve Flame ile hızlı oyun MVP geliştirmenin tüm ayrıntılarını keşfedeceğiz.
Platformlar arası geliştirme için özelliklerle dolu ve güvenli bir platform olan Flutter, mobil uygulama dünyasını kasıp kavurdu ve erişimi kullanıcı arayüzünün çok ötesine uzanıyor. Flutter üzerine kurulu sağlam ve açık kaynaklı bir oyun motoru olan Flame'in yardımıyla Android, iOS, Web ve Masaüstü cihazlarda sorunsuzca çalışan çarpıcı 2D oyunlar oluşturabilirsiniz.
Flutter, farklı cihazlarda temel işlevleri sunan çözümlerin hızlı bir şekilde geliştirilmesini kolaylaştıran entegre özellikleri nedeniyle oyun MVP'leri oluşturmak için de popüler bir çözüm haline geldi. Özellikle Flutter'ın çeşitli avantajları ve entegre işlevleri şunları sağlar:
Flutter çok fazla bilgi işlem kaynağı tüketmez ve platformlar arası uygulamaların basit kurulumunu kolaylaştırır.
Flutter ve Flame kombinasyonunu temel alan uygulama MVP'si güvenilir ancak geliştirilmesi nispeten basit bir çözümdür. Doğrudan yerel koda derlenerek sorunsuz oyun ve yanıt verme olanağı sağlar. Oyun MVP'nizi bir kez geliştirip farklı platformlara dağıtarak zamandan ve kaynaklardan tasarruf edebilirsiniz. Flutter ve Flame, kaputun altındaki platform farklılıklarını ele alıyor.
Ayrıca her iki teknoloji de kapsamlı belgeler, eğitimler ve kod örnekleriyle canlı topluluklara sahiptir. Bu, hiçbir zaman bir cevaba veya ilhama takılıp kalmayacağınız anlamına gelir.
Flame, kısa vadede ve kaynakları fazla harcamadan MVP oyun özelliklerini oluşturmak için eksiksiz bir araç seti sağlar. Bu platformlar arası modelleme çerçevesi, çok çeşitli farklı kullanım senaryolarına yönelik araçlar sunar:
Yukarıda belirtilen özelliklerin çoğu birçok oyun için gereklidir ve MVP geliştirme aşamasında bile göz ardı edilmemelidir. Gerçekten önemli olan, Flame'in yukarıda bahsedilen işlevselliği geliştirme hızını önemli ölçüde artırarak bu tür özellikleri en eski ürün sürümlerinde bile yayınlamanıza olanak sağlamasıdır.
Şimdi Flame'den bahsetmek yerine bu çerçeve ile kendi oyunumuzun temel özelliklerini içeren bir MVP oluşturalım. Başlamadan önce, test için favori IDE'niz ve cihazınız olan Flutter 3.13 veya üstünü kurmuş olmalısınız.
Bu oyun Chrome Dino'dan ilham almıştır. Ah, ünlü Dino Koşusu! Bu, Chrome'daki bir oyundan daha fazlası. Bu, tarayıcının çevrimdışı modunda gizlenmiş sevilen bir Paskalya yumurtasıdır.
Projemiz aşağıdaki oynanışa sahip olacak:
Ve buna “Orman Koşusu!” adı verilecek.
Her yeni uygulamaya başladığınızda yaptığınız gibi boş bir Flutter projesi oluşturun. Başlamak için projemiz için pubspec.yaml dosyasında bağımlılıkları ayarlamamız gerekiyor. Bu yazıyı yazarken Flame'in son sürümü 1.14.0'dır. Ayrıca şimdi tüm assetlerin yollarını tanımlayalım, böylece daha sonra bu dosyaya dönmeye gerek kalmayacak. Ve görüntüleri asset/images/ dizinine yerleştirin. Flame tam olarak bu yolu tarayacağı için buraya koymamız gerekiyor:
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/
Tüm görselleri asset/images/ altına koymayı unutmayın çünkü Flame diğer dizinleri ayrıştırmaz.
Herhangi bir oyun için çok sayıda görsele ihtiyacınız olacak. Peki ya tasarımda iyi değilseniz? Neyse ki projeleriniz için kullanabileceğiniz çok sayıda açık kaynak varlık var. Bu oyunun varlıkları itch.io'dan alınmıştır. Bu kaynakları projemiz için kullanacağız:
Bu bağlantıları ziyaret edebilir veya bu proje için hazırlanmış varlıkları (BAĞLANTI VARLIK ARŞİVİ) indirebilir ve tüm içeriği projenize kopyalayabilirsiniz.
Flame'ın Flutter'a benzer bir felsefesi var. Flutter'da her şey bir Widget'tır; Alev'de her şey bir Bileşendir, hatta Oyunun tamamı. Her Bileşen 2 yöntemi geçersiz kılabilir: onLoad() ve update(). onLoad(), Component ComponentTree'ye monte edildiğinde ve her karede update() çalıştırıldığında yalnızca bir kez çağrılır. Flutter'daki StatefulWidget'taki initState() ve build()'e çok benzer.
Şimdi biraz kod yazalım. FlameGame'i genişleten ve tüm varlıklarımızı önbelleğe yükleyen bir sınıf oluşturun.
class ForestRunGame extends FlameGame { @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); } }
Daha sonra main.dart'ta ForestRunGame'i kullanın. Ayrıca cihaz yönünü yapılandırmak için Flame.device'deki yöntemleri kullanabilirsiniz. Ve widget'lar ve bileşenler arasında köprü görevi gören GameWidget var.
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.device.fullScreen(); await Flame.device.setLandscape(); runApp(GameWidget(game: ForestRunGame())); }
Bu noktada oyuna zaten başlayabiliriz ancak yalnızca siyah bir ekran olacaktır. Bu yüzden bileşenlerimizi eklememiz gerekiyor.
Ormanı iki bileşene ayıracağız: arka plan ve ön plan. Öncelikle arka planı halledeceğiz. Hiç dinamik görünen bir sayfada gezindiniz mi? Sanki aynı anda birden fazla görünümde geziniyormuşsunuz gibi mi? Bu bir paralaks efektidir ve bir sayfanın farklı öğeleri farklı hızlarda hareket ederek 3 boyutlu bir derinlik efekti oluşturduğunda meydana gelir.
Tahmin edebileceğiniz gibi arka planımız için paralaks kullanacağız. ParallaxComponent'i genişletin ve ParallaxImageData'yı kullanarak bir görüntü yığını oluşturun. Ayrıca, ilk katmanların hızı için baseVelocity ve katmanlar arasındaki göreceli hız farkını temsil eden velocityMultiplierDelta vardır. Ve son olarak öncelik alanını (z-endeksi) diğer bileşenlerin arkasına taşıyacak şekilde yapılandırın.
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), ); } }
Arka plan tamamlandı; şimdi ön planı eklemenin zamanı geldi. Zemini ekranın alt kısmına hizalayabilmemiz için PositionComponent'i genişletin. Oyun önbelleğine erişmek için HasGameReference karışımına da ihtiyacımız var.
Zemin oluşturmak için zemin bloğu görüntüsünü birden çok kez aynı hizaya getirmeniz yeterlidir. Flame'de görüntü bileşenlerine sprite adı verilir. Sprite, bir görüntünün Canvas'ta oluşturulabilen bir bölgesidir. Görüntünün tamamını temsil edebilir veya hareketli grafik sayfasının içerdiği parçalardan biri olabilir.
Ayrıca X ekseninin sağa, Y ekseninin ise alta yönelik olduğunu unutmayın. Eksenlerin merkezi ekranın sol üst köşesine yerleştirilir.
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, ); } }
Ve son olarak bu bileşenleri ForestRunGame'imize ekleyin.
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); } }
Şimdi oyunu başlatmayı deneyin. Bu noktada zaten ormanımız var.
Orman güzel görünüyor ama şu anda sadece bir resim. Yani oyuncunun rehberliğinde bu ormanda koşacak Jack'i yaratacağız. Ağaçların ve toprağın aksine, oyuncunun canlı hissetmesi için animasyonlara ihtiyacı var. Zemin blokları için Sprite'ı kullandık, ancak Jack için SpriteAnimation'ı kullanacağız. Bu nasıl çalışır? Aslında her şey çok kolay, sadece bir dizi sprite'ı döngüye sokmanız yeterli. Örneğin, koşu animasyonumuzda küçük bir zaman aralığıyla birbirinin yerini alan 8 adet sprite vardır.
Jack koşabilir, zıplayabilir ve boşta kalabilir. Durumlarını temsil etmek için bir PlayerState listesi ekleyebiliriz. Ardından SpriteAnimationGroupComponent'i genişleten bir Player oluşturun ve PlayerState'i genel bir argüman olarak iletin. Bu bileşen, her PlayerState için animasyonların depolandığı bir animasyon alanına ve oynatıcının mevcut durumunu temsil eden, canlandırılması gereken bir geçerli alana sahiptir.
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; } }
Oyuncu durumları hazır. Şimdi oynatıcıya ekran üzerinde bir boyut ve konum vermemiz gerekiyor. Boyutunu 69x102 piksele ayarlayacağım, ancak istediğiniz gibi değiştirmekten çekinmeyin. Konum için yerin koordinatlarını bilmeliyiz. HasGameReference mixini ekleyerek ön plan alanına ulaşıp koordinatlarını alabiliyoruz. Şimdi uygulamanın boyutu her değiştiğinde çağrılan onGameResize metodunu override edip Jack'in konumunu orada ayarlayalım.
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; } }
Daha önce yapıldığı gibi, oyuncuyu Oyunumuza ekleyin.
class ForestRunGame extends FlameGame { // Earlier written code here... late final player = Player(); @override Future<void> onLoad() async { // Earlier written code here... add(player); } }
Oyuna başladığınızda Jack'in zaten ormanda olduğunu görüyorsunuz!
Oyunumuzun üç durumu vardır: giriş, oyun ve oyun bitti. Yani, bunları temsil eden GameState numaralandırmasını ekleyeceğiz. Jack'in koşmasını sağlamak için hız ve ivme değişkenlerine ihtiyacımız var. Ayrıca kat edilen mesafeyi de hesaplamamız gerekiyor (daha sonra kullanılacaktır).
Daha önce de belirtildiği gibi, Bileşenin iki ana yöntemi vardır: onLoad() ve update(). OnLoad yöntemini zaten birkaç kez kullandık. Şimdi update() hakkında konuşalım. Bu yöntemin dt adı verilen bir parametresi vardır. update()'in son çağrılmasından itibaren geçen süreyi temsil eder.
Mevcut hızı ve kat edilen mesafeyi hesaplamak için update() yöntemini ve bazı temel kinematik formüllerini kullanacağız:
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; } } } }
Aslında geliştirmeyi kolaylaştırmak için bir numara kullanacağız: Jack sabit kalacak ama orman Jack'e doğru ilerleyecek. Bu yüzden oyun hızını uygulamak için ormanımıza ihtiyacımız var.
Paralaks arka planı için sadece oyun hızını geçmemiz gerekiyor. Ve gerisini otomatik olarak halledecektir.
class ForestBackground extends ParallaxComponent<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); parallax?.baseVelocity = Vector2(game.currentSpeed / 10, 0); } }
Ön plan için her yer bloğunu kaydırmamız gerekiyor. Ayrıca sıradaki ilk bloğun ekrandan çıkıp çıkmadığını da kontrol etmemiz gerekiyor. Eğer öyleyse, onu çıkarın ve kuyruğun sonuna koyun;
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); } } }
Her şey hazır ama bir tetikleyici. Tıklandığında koşmaya başlamak istiyoruz. Hedeflerimiz hem mobil hem de masaüstü olduğundan ekran dokunmalarını ve klavye olaylarını ele almak istiyoruz.
Neyse ki Flame'in bunu yapmanın bir yolu var. Giriş türünüz için bir karışım eklemeniz yeterli. Klavye için, ekrana dokunmak için KeyboardEvents ve TapCallbacks kullanılır. Bu karışımlar size ilgili yöntemleri geçersiz kılma ve mantığınızı sağlama olanağı verir.
Kullanıcı boşluk çubuğuna basarsa veya ekrana dokunursa oyun başlamalıdır.
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; } }
Sonuç olarak Jack tıkladıktan sonra artık koşabilir.
Artık yolda engellerin olmasını istiyoruz. Bizim durumumuzda zehirli çalılar olarak temsil edilecekler. Bush animasyonlu olmadığından SpriteComponent'i kullanabiliriz. Ayrıca hızına erişmek için bir oyun referansına ihtiyacımız var. Ve bir şey daha; çalıları tek tek oluşturmak istemiyoruz çünkü bu yaklaşım Jack'in bir sıra çalıyı sıçrayarak geçemeyeceği bir duruma neden olabilir. Bu, mevcut oyun hızına bağlı olarak aralıktaki rastgele bir sayıdır.
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(); } } }
Kim çalı dikiyor? Tabi ki doğa. Çalı neslimizi yönetecek Doğa'yı yaratalım.
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()); } } } }
Şimdi ForestForeground'a Doğa'yı ekleyelim.
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); }
Artık ormanımızda çalılar var. Ama durun, Jack sadece onların arasından geçiyor. Bu neden oluyor? Çünkü henüz vurmayı uygulamadık.
Burada Hitbox bize yardımcı olacak. Hitbox, Flame'in bileşen hayvanat bahçesindeki başka bir bileşendir. Çarpışma tespitini kapsar ve size bunu özel mantıkla halletme imkanı verir.
Jack için bir tane ekle. Bileşenin konumunun orta değil sol-sağ köşeye yerleştirileceğini unutmayın. Ve boyuta gelince, gerisini siz halledersiniz.
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), ), ); } }
Ve bir tane de çalılık için. Burada, bazı optimizasyonlar için çarpışma türünü pasif olarak ayarlayacağız. Varsayılan olarak tür etkindir; bu, Flame'in bu hitbox'ın diğer tüm hitbox'larla çarpışıp çarpışmadığını kontrol edeceği anlamına gelir. Sadece bir oyuncumuz ve çalılarımız var. Oyuncunun zaten aktif bir çarpışma türü olduğundan ve çalılar birbiriyle çarpışamayacağından türü pasif olarak ayarlayabiliriz.
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, ), ); } }
Harika ama isabet kutusunun konumunun doğru ayarlanıp ayarlanmadığını göremiyorum. Nasıl test edebilirim?
Player ve Bush'un debugMode alanını true olarak ayarlayabilirsiniz. Hitbox'larınızın nasıl konumlandırıldığını görmenizi sağlayacaktır. Mor, bileşenin boyutunu tanımlar ve sarı, isabet alanını belirtir.
Şimdi oyuncu ile çalı arasında ne zaman bir çarpışma olduğunu tespit etmek istiyoruz. Bunun için Game'e HasCollisionDetection karışımını ve ardından çarpışmayı işlemesi gereken bileşenler için CollisionCallbacks'i eklemeniz gerekir.
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... }
Şimdilik, çarpışma algılandığında oyunu duraklatmanız yeterli.
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; } }
Jack o çalılardan kaçınmak istiyorsa atlaması gerekiyor. Ona öğretelim. Bu özellik için yerçekimi sabitine ve Jack'in sıçramasının başlangıçtaki dikey hızına ihtiyacımız var. Bu değerler gözle seçilmiştir, bu nedenle bunları ayarlamaktan çekinmeyin.
Peki yerçekimi nasıl çalışır? Temel olarak aynı ivmeye sahiptir ancak yere yöneliktir. Yani aynı formülleri dikey konum ve hız için de kullanabiliriz. Yani atlamamızın 3 adımı olacak:
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; } } }
Şimdi ForestRunGame'den tıklamayla zıplamayı tetikleyelim
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; } } }
Artık Jack çalıları idare edebiliyor.
Oyun bittiğinde ekranda yazı göstermek istiyoruz. Alevdeki Metin Flutter'dan farklı çalışır. Önce bir yazı tipi oluşturmanız gerekir. Kaputun altında, bu sadece char'ın bir anahtar ve sprite'ın bir değer olduğu bir haritadır. Neredeyse her zaman oyunun yazı tipi, gerekli tüm sembollerin toplandığı tek bir resimdir.
Bu oyun için sadece rakamlara ve büyük harflere ihtiyacımız var. O halde yazı tipimizi oluşturalım. Bunu yapmak için kaynak görüntüyü ve glifleri aktarmanız gerekir. Glif nedir? Glif, karakter, boyutu ve kaynak görüntüdeki konumu hakkındaki bilgilerin birleşimidir.
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); }
Artık oyunu panel üzerinden oluşturup oyun içinde kullanabiliriz.
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; } }
Artık Jack çalılığa çarptığında panelimizi gösterebiliriz. Ayrıca start() metodunu da değiştirelim, böylece tıklandığında oyunu yeniden başlatabiliriz. Ayrıca ormandaki tüm çalıları temizlememiz gerekiyor.
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; } }
Şimdi oynatıcıdaki çarpışma geri çağrısını güncellememiz gerekiyor.
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(); } }
Artık Jack bir çalılığa çarptığında Game Over'ı görebilirsiniz. Ve tekrar tıklayarak oyunu yeniden başlatın.
Ve son dokunma puanı hesaplaması.
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'); } }
Hepsi bu kadar millet!
Şimdi deneyin ve yüksek puanımı geçmeye çalışın. 2537 puan!
Çok oldu ama başardık. Fizik, animasyonlar, puan hesaplama ve çok daha fazlasını içeren bir mobil oyun için minimum uygulanabilir bir ürün oluşturduk. Her zaman iyileştirmeye yer vardır ve tıpkı diğer MVP'ler gibi bizim ürünümüz de gelecekte yeni özellikler, mekanikler ve oyun modlarıyla beklenebilir.
Ayrıca arka plan müziği, atlama veya vurma sesleri vb. eklemek için kullanabileceğiniz bir flame_audio paketi de bulunmaktadır.
Şimdilik temel amacımız kısa vadede ve sınırlı kaynak tahsisiyle temel ürün işlevselliğini oluşturmaktı. Flutter ve Flame kombinasyonunun, kullanıcı geri bildirimlerini toplamak ve gelecekte uygulamayı yükseltmeye devam etmek için kullanılabilecek bir oyun MVP'si oluşturmak için mükemmel bir uyum olduğu kanıtlandı.
Çabalarımızın sonuçlarını buradan kontrol edebilirsiniz.
Güçlü özellikleri, kullanım kolaylığı ve gelişen topluluğuyla Flutter ve Flame, gelecek vaat eden oyun geliştiricileri için ilgi çekici bir seçimdir. İster deneyimli bir profesyonel olun ister yeni başlıyor olun, bu kombinasyon oyun fikirlerinizi hayata geçirecek araçları ve potansiyeli sunar. Öyleyse yaratıcılığınızı kapın, Flutter ve Flame dünyasına dalın ve bir sonraki mobil oyun heyecanını yaratmaya başlayın!
Bu makaleyi eğlenceli ve bilgilendirici bulduğunuzu umuyoruz. Yazılım geliştirmeyle ilgili daha fazla bilgi edinmek istiyorsanız veya kendi MVP projenizi tartışmak istiyorsanız Leobit'i keşfetmekten veya teknik ekibimizle iletişime geçmekten çekinmeyin!