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.
Öncelikle yeni bir proje oluşturalım. Resmi Bare Flame oyun öğreticisi, bunu yapmak için gereken tüm adımları açıklamakta harika bir iş çıkarıyor; bu yüzden onu takip etmeniz yeterli.
Eklemeniz gereken bir şey var: pubspec.yaml
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. ( düzeltme işareti sözdizimi )
Tüm adımları izlediyseniz main.dart
dosyanız şöyle görünmelidir:
import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); }
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. GrafxKid'in Arcade Platformer Assets paketini kullanacağım.
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ı ( doku paketleyici gibi) 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.
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 Nearest Neighbour (hard edges)
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.
Açıklamalara gerek kalmadan benim hazırladığım varlıkları indirin veya kendiniz hazırlayıp projenizin assets/images
klasörüne ekleyin.
Yeni varlıklar eklediğinizde, bunları pubspec.yaml
dosyasına şu şekilde kaydetmeniz gerekir:
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 lib
dizininde yeni bir dosya oluşturalım: 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); } }
PlatformerGame
, oyunumuzu temsil eden ana sınıftır ve Flame motorunda kullanılan temel oyun sınıfı olan FlameGame
genişletir. Bu da Alev'in temel yapı taşı olan Component
genişletir. Görüntüler, arayüz ve efektler de dahil olmak üzere oyununuzdaki her şey Bileşenlerdir. Her Component
bileşen başlatıldığında çağrılan bir onLoad
zaman uyumsuz yöntemi vardır. Genellikle tüm bileşen kurulum mantığı oraya gider.
Son olarak, varlık sabitlerimizin nereden geldiğini açıkça belirtmek için daha önce oluşturduğumuz ve as Assets
eklediğimiz assets.dart
dosyamızı içe aktardık. Ve SPRITES
listesinde listelenen tüm varlıkları oyun görüntüleri önbelleğine yüklemek için images.loadAll
yöntemini kullandı.
Daha sonra main.dart
yeni PlatformerGame
oluşturmamız gerekiyor. Dosyayı aşağıdaki gibi değiştirin:
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.
Yeni bir lib/actors/
klasörü ve bunun içinde yeni bir theboy.dart
dosyası oluşturun. Bu, oyuncu karakterini temsil eden bileşen olacak: The Boy.
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 SpriteAnimationComponent
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 HasGameRef
karışımına sahiptir.
onLoad
yöntemimizde, assets.dart
dosyasında bildirdiğimiz THE_BOY
sprite sayfasından yeni bir SpriteAnimation
oluşturuyoruz.
Şimdi oynatıcımızı oyuna ekleyelim! game.dart
dosyasına dönün ve onLoad
yönteminin altına aşağıdakileri ekleyin:
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!
Öncelikle The Boy'u klavyeden kontrol etme özelliğini eklememiz gerekiyor. game.dart
dosyasına HasKeyboardHandlerComponents
mix'ini ekleyelim.
class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents
Sonra theboy.dart
ve KeyboardHandler
karışımına dönelim:
class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame>
Ardından TheBoy
bileşenine bazı yeni sınıf değişkenleri ekleyin:
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 onKeyEvent
yöntemini geçersiz kılalım:
@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 _horizontalDirection
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. update
yöntemini ekleyerek bunu düzeltelim.
Ş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 Component's
yöntem render
işlenir ve ardından yöntem update
yeni bir durum hesaplanır. Yöntemin imzasındaki dt
parametresi, son durum güncellemesinden bu yana geçen milisaniye cinsinden süredir. Bunu aklınızda tutarak theboy.dart
aşağıdakileri ekleyin:
@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 dt
ile çarpılmasıyla değiştiririz.
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 update
yönteminin altına ekleyin:
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 onLoad
şu şekilde güncelleyin:
@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 updateAnimation
yöntemi ekleyelim:
void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } }
Ve son olarak update
yönteminin altındaki bu yöntemi çağırın ve oyunu çalıştırın.
İ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 github'ımda bulabilirsiniz.
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!
Her bölümün sonuna harika yaratıcıların ve öğrendiğim kaynakların bir listesini ekleyeceğim.