Geçen bölümde proje klasörünü düzeledik, modülleri oluşturduk ve ana oyun sınıfını hazırladık. Bu bölümde bir oyuncu oluşturup klavye ile yönlendirilmesini sağlayacağız.

Grafikler

Kaplama (texture) olmayan resimlere sprite deniyor. Birden fazla sprite'ın tek dosyada olmasına da spritesheet. Oyuncunun dört tarafa da dönmüş halinin olduğu bir spritesheet'e ihtiyacımız var. Sprite Generator adresinden kolayca karakter tasarlayabilir ya da OpenGameArt'tan beğendiğiniz bir tanesini kullanabilirsiniz.

2D Character Spritesheet
Örnek bir spritesheet. Şimdilik bunu kullanabilirsiniz...

Harici dosyaların yönetimi

Oyun içinde kullanacağımız dosyalara merkezi bir yerden ulaşmak ve performansı artırmak için bir sınıf yazacağız; ResourceManager. Bu sayede hem daha okunabilir kod olacak hem de tüm dosyalar bir kere yüklenip kullanılacağı için daha hızlı olacak.

Şimdi proje klasöründeki core / managers / resourcemanager.py içine gelip aşağıdaki sınıfı oluşturun. Şimdilik sadece bir tane dosya var. İleride lazım oldukça ekleyeceğiz.

import os.path

import pygame


class ResourceManager(object):
    path = os.path.abspath(__file__ + '/../../../data')

    sprites = {
        'player': pygame.image.load('{}{}'.format(path, '/sprites/player.png'))
    }

Önce abspath fonksiyonu yardımı ile dosya konumundan üç klasör üste çıkıp, o dizindeki data klasörünün adresini aldık. Ardından sprites sözlüğü içindeki player elemanına data / sprites içindeki player.png dosyasının (az önce indirdiğiniz spritesheet) adresini verdik. Yani proje-dizini / data / sprites / player.png adresini.

İlerleyen bölümlerde bu sınıf içinde tiles, maps gibi sözlükler de oluşturacağız. Şimdilik bu kadarı yeterli.

Varlıklar

Oyun içindeki tüm varlıklar (oyuncular, düşmanlar, binalar, bitkiler, aletler vs.) için base (taban, temel) sınıf oluşturalım. Bu sınıfın görevi şöyle olacak;

Nesnenin koordinat ve boyut bilgilerini al.
Animasyon için verilen spritesheet dosyasını al.
Varsa nesneye ait aksiyonları al. (örneğin hareket etme)
Belirlenen hıza göre spritesheet içinden gerekli bölümü kes.
Hazırlanan animasyon karesini (frame) ekrana bas.

Şimdi core / __init__.py içinde aşağıdaki sınıfı oluşturun.

import time

from pygame import Rect


class Entity(object):
    def __init__(self, sheet, rect, actions=None):
        self.sheet = sheet
        self.rect = rect

        if actions:
            self.actions = actions

    offset = (0, 0)
    fps = 0.1
    updated_at = time.time()
    animating = False

    def update(self):
        self.animate()

    def animate(self):
        if time.time() - self.updated_at > self.fps and self.animating:
            x, y = self.offset
            if x < 3:
                x += 1
            else:
                x = 0
            self.offset = (x, y)

            self.updated_at = time.time()

    @property
    def image(self):
        x, y = self.offset
        width, height = self.size

        self.sheet.set_clip(
            Rect(
                x * width,
                y * height,
                width, height
            )
        )

        return self.sheet.subsurface(self.sheet.get_clip())

    @property
    def size(self):
        return self.rect.width, self.rect.height

    @property
    def location(self):
        return self.rect.left, self.rect.top

    @location.setter
    def location(self, value):
        x, y = value
        self.rect.left = x
        self.rect.top = y

    def render(self, surface):
        surface.blit(self.image, self.rect)

Sınıf içindeki offset demeti spritesheet içinden alınması gereken bölümü (satır, sütun) olarak tutuyor. Varsayılan olarak (0, 0) yani sol üstten ilk kare. Sınıfın image özelliğinde satır ve sütun numaraları genişlik ve yükseklik değerleriyle çarpılıp kesilmeye başlanması gereken pikseller bulunuyor.

Rect nesnesi içinde demet halinde, x, y, genişlik ve yükseklik bilgilerini tutuyor. Sınıfın size özelliği genişlik ve yüksekliği, location özelliği x ve y değerlerini veriyor.

Animasyon hızını ayarlamak için fps, durdurmak ya da devam ettirmek için animating, son değişiklik yapılan zamanı tutmak için updated_at kullanılıyor.

Gösterilecek bölümü (x, y) belirlemek için animate fonksiyonunda şimdinin zamanından son değişiklik zamanı çıkarılıyor. Eğer sonuç fps değerinden büyüksek ve nesne animasyon (animating) halinde ise offset değerini yatayda x += 1 artırıyor. Eğer yatay offset 3'ten büyükse (ki bu resmin dışında demektir) tekrar sıfıra eşitliyor. Bu şekilde bir animasyon döngüsü elde ediliyor. Bu üç rakamı sabit değil tabi kullanılan spritesheet'teki frame sayısına göre değişir. Şimdilik 3 işimizi görüyor, ileride üzerinde oynama yaparız.

Sınıfın image özelliğinde spritesheet içinden kesilmesi gereken bölüm offset ve size özelliklerinin çarpılmasıyla bulunduktan sonra set_clip fonksiyonu ile işaretleniyor. Ardından get_clip ile kesip alınan bölüm subsurface ile spritesheet içinden çıkartırıp return ile döndürülüyor.

Render fonksiyonu parametre olarak aldığı surface içine (Game sınıfındaki self.screen) nesne resmini (self.image) koordinat ve boyut bilgilerini (self.rect) kullanarak çiziyor.

Karakterler

Oyuncu, NPC ya da hikaye kahramanları için base (taban) sınıf oluşturalım. Bu sınıf Entity nesnesinin tüm özelliklerini miras alacak.

Şimdi core / characters / __init__.py içinde aşağıdaki sınıfı oluşturun.

from .. import Entity


class Character(Entity):
    speed = 4

    def __init__(self, actions, sheet, rect):
        super(Character, self).__init__(
            actions=actions,
            sheet=sheet,
            rect=rect
        )

    def do(self, keys):
        self.animating = False

        for key in self.actions.keys():
            if keys.get(key, False):
                params = self.actions.get(key)
                params.get('func')(
                    params.get('args')
                )

                x, y = self.offset
                y = params.get('offset', y)
                self.offset = (x, y)

                self.animating = True

Karakterin hareket hızını belirlemek için speed değişkenini kullanacağız. Yapıcı fonksiyonda (__init__) gelen parametreleri super fonksiyonu ile Entity sınıfına yollamaktan başka birşey yapmadık.

Nesneye ait aksiyonları do fonksiyonu işleyecek. Önce animasyonu durduruyor, sonra actions sözlüğündeki tüm anahtarları tek tek geziyor ve eğer keys sözlüğü içindeki değeri True ise sözlük içindeki fonksiyona, yine sözlük içindeki args parametrelerini gönderiyor.

Ardından sözlükteki offset anahtarını alıp kendi offset değerinin dikey pozisyonu (y) ile değiştiriyor. Ardından animasyona devam ettiriyor.

Bu kısmın biraz havada kaldığının farkındayım. Amacı, her karaktere birbirinden farklı özellikler verebilmek. Az sonra oyuncu sınıfını yazarken anlayacaksınızdır.

Oyuncu

Buraya kadar biraz sıkılmış olabilirsiniz. Ama bir sonuca ulaşmaya çok az kaldı. Şimdi kullanıcının yönlendireceği karakteri oluşturacağız, core / characters / player.py içinde aşağıdaki sınıfı oluşturun.

import pygame

from . import Character
from ..managers.resourcemanager import ResourceManager


class Player(Character):
    speed = 4

    def __init__(self):
        self.rect = pygame.Rect(0, 0, 32, 48)
        actions = {
            '273': {'func': self.move, 'args': (0, -self.speed), 'offset': 3},   # K_UP
            '275': {'func': self.move, 'args': (self.speed, 0),  'offset': 2},   # K_RIGHT
            '274': {'func': self.move, 'args': (0, self.speed),  'offset': 0},   # K_DOWN
            '276': {'func': self.move, 'args': (-self.speed, 0), 'offset': 1},   # K_LEFT
        }

        super(Player, self).__init__(
            actions=actions,
            sheet=ResourceManager.sprites.get('player'),
            rect=self.rect
        )

    def move(self, direction):
        x, y = self.location
        horizontaly, vertically = direction
        x += horizontaly
        y += vertically
        self.location = (x, y)

Önce self.rect ile oyuncunun konumunu (0, 0) ve boyutlarını (32 x 48) belirledik. Ardından az özce oluşturduğumuz Character sınıfının kullanması için actions sözlüğünü oluşturduk.

Sözlük içindeki 273, 275, 274, 276 anahtarları sırasıyla yukarı, sağ, aşağı ve sol ok tuşlarını ifade ediyor. Her anahtarın içinde birer tane daha sözlük bulunuyor. Bunlara iç-sözlük diyelim.

İç-sözlüğün 'func' anahtarı aksiyon için gerekli olan fonksiyonu gösteriyor, 'args' ise bu fonksiyonun parametrelerini. X ve y değerlerini speed kadar artırıp azaltarak oyuncunun hareket etmesini sağladık. Son olarak 'offset' sprite içindeki dikey konumu ifade ediyor.

Daha önce de olduğu gibi alınan parametreleri super fonksiyonuyla ebeveyn sınıfa (Character) gönderdik. Character sınıfından farklı olarak bu sefer sheet değerini parametre olarak almayıp ResourceManager içinden çağırdık.

Yani aldığımız dosyanın yolculuğu şöyle olacak; ResourceManager > Player > Character > Entity.

Son olarak actions sözlüğündeki offset verisini işlemek üzere move fonksiyonunu tanımladık. Burada basitçe oyuncuyu verilen yöne doğru hareket ettiriyoruz.

Oyuncular Sahneye

Şimdi, main.py içinde Player sınıfını import edin.

from core.characters.player import Player

Ardından Game sınıfı içinde yeni bir Player örneği oluşturun.

class Game:
    def __init__(self, display, flags, depth):
        pygame.init()
        pygame.display.set_caption("2D RPG")
        self.screen = pygame.display.set_mode(display, flags, depth)
        self.running = True

    clock = pygame.time.Clock()
    fps = 30

    player = Player()

...

Son olarak render fonksiyonu içinde oyuncunun render'ını çağırın.

...

    def render(self):
        self.screen.fill((255, 255, 255))

        self.player.render(self.screen)

        pygame.display.update()

...

İşte bu kadar! Şimdi python main.py ile oyunu başlatın ve karakteri ok tuşları ile yönlendirmeyi deneyin.

Pygame 2D Oyun
Hiçbir sorun yoksa oyun buna benzer görünüyor olmalı.

Kolay gelsin!