Her zaman video oyunları yapmak istemiştim. İlk işimi almama yardımcı olan ilk Android uygulamam, Android görünümleriyle yapılan basit bir oyundu. Bundan sonra, bir oyun motoru kullanarak daha ayrıntılı bir oyun yaratmak için pek çok girişimde bulunuldu, ancak bunların hepsi zaman eksikliği veya çerçevenin karmaşıklığı nedeniyle başarısız oldu. Ancak Flutter'ı temel alan Flame motorunu ilk duyduğumda, basitliği ve platformlar arası desteği ilgimi çekti ve onunla bir oyun geliştirmeyi denemeye karar verdim. Motoru anlamak için basit ama yine de zorlayıcı bir şeyle başlamak istedim. Bu makale dizisi, Flame'i (ve Flutter'ı) öğrenme ve temel bir platform oyunu geliştirme yolculuğumdur. Bunu oldukça ayrıntılı hale getirmeye çalışacağım, bu nedenle Flame'e veya genel olarak oyun geliştirmeye yeni başlayan herkes için faydalı olacaktır. 4 makale boyunca aşağıdakileri içeren 2 boyutlu bir yan kaydırma oyunu geliştireceğim: Koşabilen ve zıplayabilen bir karakter Oyuncuyu takip eden bir kamera Zemin ve platformlarla birlikte kayan seviye haritası Paralaks arka planı Oyuncunun toplayabileceği paralar ve jeton sayısını gösteren HUD Kazanma ekranı İlk bölümde yeni bir Flame projesi oluşturacağız, tüm varlıkları yükleyeceğiz, oyuncu karakteri ekleyeceğiz ve ona nasıl koşacağını öğreteceğiz. Proje kurulumu Öncelikle yeni bir proje oluşturalım. Resmi öğreticisi, bunu yapmak için gereken tüm adımları açıklamakta harika bir iş çıkarıyor; bu yüzden onu takip etmeniz yeterli. Bare Flame oyun Eklemeniz gereken bir şey var: dosyasını ayarlarken, kitaplık sürümlerini mevcut en son sürüme güncelleyebilir veya olduğu gibi bırakabilirsiniz; çünkü bir sürümün önündeki düzeltme işareti (^), uygulamanızın en yeni olmayan en son sürümü kullanmasını sağlayacaktır. -kırılma versiyonu. ( ) pubspec.yaml düzeltme işareti sözdizimi Tüm adımları izlediyseniz dosyanız şöyle görünmelidir: main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); } Varlıklar Devam etmeden önce oyun için kullanılacak varlıkları hazırlamamız gerekiyor. Varlıklar; resimler, animasyonlar, sesler vb.'dir. Bu serinin amaçları doğrultusunda, yalnızca oyun geliştirmede sprite olarak da adlandırılan görselleri kullanacağız. Bir platform seviyesi oluşturmanın en basit yolu, döşeme haritalarını ve döşeme spritelarını kullanmaktır. Bu, seviyenin temelde her hücrenin hangi nesneyi / zemini / platformu temsil ettiğini gösterdiği bir ızgara olduğu anlamına gelir. Daha sonra oyun çalışırken, her hücreden gelen bilgiler ilgili kare grafiğiyle eşleştirilir. Bu teknik kullanılarak oluşturulan oyun grafikleri gerçekten ayrıntılı veya çok basit olabilir. Mesela Super Mario bros'ta pek çok unsurun tekrarlandığını görüyorsunuz. Bunun nedeni, oyun tablosundaki her zemin döşemesi için onu temsil eden yalnızca bir zemin görselinin bulunmasıdır. Biz de aynı yaklaşımı izleyeceğiz ve elimizdeki her statik nesne için tek bir görsel hazırlayacağız. Ayrıca oyuncu karakteri ve madeni paralar gibi bazı nesnelerin canlandırılmasını da istiyoruz. Animasyon genellikle her biri tek bir kareyi temsil eden bir dizi hareketsiz görüntü olarak saklanır. Animasyon oynatılırken kareler birbiri ardına hareket ederek nesnenin hareket ettiği yanılsamasını yaratır. Şimdi en önemli soru varlıkların nereden alınacağıdır. Elbette bunları kendiniz çizebilir veya bir sanatçıya sipariş verebilirsiniz. Ayrıca oyun varlıklarını açık kaynağa katkıda bulunan birçok harika sanatçı var. kullanacağım. GrafxKid'in Arcade Platformer Assets paketini Tipik olarak görüntü varlıkları iki biçimde gelir: hareketli grafik sayfaları ve tekli hareketli görüntüler. İlki, tüm oyun varlıklarını bir arada içeren büyük bir resimdir. Daha sonra oyun geliştiricileri gerekli hareketli grafiğin tam konumunu belirler ve oyun motoru onu sayfadan keser. Bu oyun için tek hareketli karakterler kullanacağım (animasyonlar hariç, bunları tek bir görüntü olarak tutmak daha kolaydır) çünkü hareketli grafik sayfasında sağlanan tüm varlıklara ihtiyacım yok. İster kendiniz yaratıyor olun ister bir sanatçıdan alıyor olun, oyun motoruna daha uygun hale getirmek için onları dilimlemeniz gerekebilir. Bu amaç için özel olarak oluşturulmuş araçları ( veya herhangi bir grafik düzenleyiciyi kullanabilirsiniz. Adobe Photoshop kullandım, çünkü bu hareketli grafik sayfasında hareketli görüntüler arasında eşit olmayan boşluklar var, bu da otomatik araçların görüntüleri çıkarmasını zorlaştırıyordu, bu yüzden bunu manuel olarak yapmak zorunda kaldım. doku paketleyici gibi) Ayrıca varlıkların boyutunu da artırmak isteyebilirsiniz, ancak bu bir vektör görüntüsü değilse ortaya çıkan hareketli grafik bulanıklaşabilir. Piksel sanatı için harika sonuç veren bulduğum bir geçici çözüm, Photoshop'ta yeniden boyutlandırma yöntemini kullanmaktır (veya Gimp'te Enterpolasyon Yok olarak ayarlanmıştır). Ancak varlığınız daha ayrıntılıysa muhtemelen işe yaramayacaktır. Nearest Neighbour (hard edges) Açıklamalara gerek kalmadan indirin veya kendiniz hazırlayıp projenizin klasörüne ekleyin. benim hazırladığım varlıkları assets/images Yeni varlıklar eklediğinizde, bunları dosyasına şu şekilde kaydetmeniz gerekir: pubspec.yaml flutter: assets: - assets/images/ Geleceğe yönelik ipucu: Halihazırda kayıtlı varlıkları güncelliyorsanız değişiklikleri görmek için oyunu yeniden başlatmanız gerekir. Şimdi varlıkları oyuna yükleyelim. Tüm varlık adlarının tek bir yerde olmasını seviyorum; bu, küçük bir oyun için harika çalışıyor çünkü her şeyi takip etmek ve gerektiğinde değişiklik yapmak daha kolay. Öyleyse dizininde yeni bir dosya oluşturalım: 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]; Daha sonra gelecekte tüm oyun mantığını içerecek başka bir dosya oluşturun: 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); } } , oyunumuzu temsil eden ana sınıftır ve Flame motorunda kullanılan temel oyun sınıfı olan genişletir. Bu da Alev'in temel yapı taşı olan genişletir. Görüntüler, arayüz ve efektler de dahil olmak üzere oyununuzdaki her şey Bileşenlerdir. Her bileşen başlatıldığında çağrılan bir zaman uyumsuz yöntemi vardır. Genellikle tüm bileşen kurulum mantığı oraya gider. PlatformerGame FlameGame Component Component onLoad Son olarak, varlık sabitlerimizin nereden geldiğini açıkça belirtmek için daha önce oluşturduğumuz ve eklediğimiz dosyamızı içe aktardık. Ve listesinde listelenen tüm varlıkları oyun görüntüleri önbelleğine yüklemek için yöntemini kullandı. as Assets assets.dart SPRITES images.loadAll Daha sonra yeni oluşturmamız gerekiyor. Dosyayı aşağıdaki gibi değiştirin: 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, ), ); } Tüm hazırlıklar tamamlanıyor ve işin eğlenceli kısmı başlıyor. Oyuncu karakteri ekleme Yeni bir klasörü ve bunun içinde yeni bir dosyası oluşturun. Bu, oyuncu karakterini temsil eden bileşen olacak: The Boy. 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 ), ); } } Sınıf, animasyonlu sprite'lar için kullanılan bir bileşen olan genişletir ve daha sonra oyun önbelleğinden görüntüler yüklemek veya genel değişkenler almak için oyun nesnesine referans vermemize olanak tanıyan karışımına sahiptir. SpriteAnimationComponent HasGameRef yöntemimizde, dosyasında bildirdiğimiz sprite sayfasından yeni bir oluşturuyoruz. onLoad assets.dart THE_BOY SpriteAnimation Şimdi oynatıcımızı oyuna ekleyelim! dosyasına dönün ve yönteminin altına aşağıdakileri ekleyin: game.dart onLoad final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy); Oyunu şimdi çalıştırırsanız The Boy'la tanışabiliriz! Oyuncu hareketi Öncelikle The Boy'u klavyeden kontrol etme özelliğini eklememiz gerekiyor. dosyasına mix'ini ekleyelim. game.dart HasKeyboardHandlerComponents class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents Sonra ve karışımına dönelim: theboy.dart KeyboardHandler class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame> Ardından bileşenine bazı yeni sınıf değişkenleri ekleyin: 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 Son olarak klavye girişlerini dinlemeye izin veren yöntemini geçersiz kılalım: 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; } Artık oyuncu sağa hareket ederse 1'e, oyuncu sola hareket ederse -1'e ve oyuncu hareket etmezse 0'a eşittir. Ancak oyuncunun pozisyonu henüz değişmediği için bunu henüz ekranda göremiyoruz. yöntemini ekleyerek bunu düzeltelim. _horizontalDirection update Şimdi oyun döngüsünün ne olduğunu açıklamam gerekiyor. Temel olarak bu, oyunun sonsuz bir döngüde çalıştırıldığı anlamına gelir. Her yinelemede, mevcut durum yöntem işlenir ve ardından yöntem yeni bir durum hesaplanır. Yöntemin imzasındaki parametresi, son durum güncellemesinden bu yana geçen milisaniye cinsinden süredir. Bunu aklınızda tutarak aşağıdakileri ekleyin: Component's render update dt theboy.dart @override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; } Her oyun döngüsü döngüsü için, mevcut yönü ve maksimum hızı kullanarak yatay hızı güncelleriz. Daha sonra hareketli grafiğin konumunu güncellenmiş değerin ile çarpılmasıyla değiştiririz. dt Neden son kısma ihtiyacımız var? Eğer konumu sadece hızla güncellerseniz, o zaman sprite uzaya doğru uçup gidecektir. Ama daha küçük olan hız değerini kullanabilir miyiz diye sorabilirsiniz. Yapabiliriz, ancak oyuncunun hareket etme şekli farklı saniye başına kare (FPS) oranları nedeniyle farklı olacaktır. Saniyedeki kare (veya oyun döngüleri) sayısı, oyun performansına ve çalıştırıldığı donanıma bağlıdır. Cihaz performansı ne kadar iyi olursa, FPS o kadar yüksek olur ve oynatıcı o kadar hızlı hareket eder. Bunu önlemek için hızı son kareden geçen süreye bağlı hale getiriyoruz. Bu şekilde sprite herhangi bir FPS'de benzer şekilde hareket edecektir. Tamam, eğer oyunu şimdi çalıştırırsak şunu görmeliyiz: Harika, şimdi çocuğun sola gittiğinde dönmesini sağlayalım. Bunu yönteminin altına ekleyin: update if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); } Oldukça kolay mantık: Mevcut yönün (kullanıcının bastığı ok) hareketli grafiğin yönünden farklı olup olmadığını kontrol ederiz, ardından hareketli grafiği yatay eksen boyunca çeviririz. Şimdi koşu animasyonunu da ekleyelim. İlk önce iki yeni sınıf değişkenini tanımlayın: late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation; Daha sonra şu şekilde güncelleyin: 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; } Burada daha önce eklediğimiz boşta kalma animasyonunu sınıf değişkenine çıkardık ve yeni bir çalıştırma animasyonu değişkeni tanımladık. Sonra yeni bir yöntemi ekleyelim: updateAnimation void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } } Ve son olarak yönteminin altındaki bu yöntemi çağırın ve oyunu çalıştırın. update Çözüm İlk bölüm için bu kadar. Flame oyununun nasıl kurulacağını, varlıkların nerede bulunacağını, bunları oyununuza nasıl yükleyeceğinizi ve harika bir animasyonlu karakterin nasıl oluşturulacağını ve klavye girişlerine göre nasıl hareket ettirileceğini öğrendik. Bu bölümün kodunu bulabilirsiniz. github'ımda Bir sonraki makalede, Tiled kullanarak nasıl oyun seviyesi oluşturulacağını, Alev kamerasını nasıl kontrol edeceğinizi ve paralaks arka planının nasıl ekleneceğini ele alacağım. Bizi izlemeye devam edin! Kaynaklar Her bölümün sonuna harika yaratıcıların ve öğrendiğim kaynakların bir listesini ekleyeceğim. GrafxKid'in Arcade platform varlıkları https://opengameart.org/content/arcade-platformer-assets DevKage Flame Oyun Geliştirme Serisi: https://youtu.be/mSPalRqZQS8 Craig Oda kanalı https://youtu.be/hwQpBuZoV9s Ember Quest Oyun Eğitimi https://github.com/flame-engine/flame/blob/main/doc/tutorials/platformer/platformer.md Alev Motoru belgeleri https://docs.flame-engine.org/1.6.0/flame/flame.html