Bu yazıda iki boyutlu ve izometrik bir oyunda karakterinin yürüme animasyonunun nasıl yapılacağının mantığı ve SDL ile bunun kodlaması gösterilmektedir. Genel olarak bir oyun döngüsü (ilkle - güncelle - çiz) hakkında bilginiz olduğu ve kodlama kısmında SDL'e en azından giriş yaptığınız varsayılmaktadır. (Yapmadıysanız bkz. Linux-Mint veya Ubuntu üzerine SDL kurulumu)
Yapılacak işlemler sırasıyla:
Sprite sheet
Animasyonun her bir karesinin ayrı bir resim dosyası olarak tutulması, daha fazla bellek kullanımına yol açar. Ayrıca dosyaların bellekte dağınık durması çalışma zamanında performansı olumsuz etkiler.
Sprite sheet (animasyon sayfası) kullanmanın mantığı, animasyonun tüm karelerinin tek büyük bir sayfada sıkıştırılıp yeri geldiğinde bu sayfanın ilgili kısımlarını ekrana basmaktır.
Yapılacak işlemler sırasıyla:
- İzometrik yürüme animasyonunun sprite sheet'inin anlamlı şekilde düzenlenmesi,
- Klavye girdisine göre karakterin hız ve yön vektörlerinin belirlenmesi,
- Karakterin yön ve hareket durumuna göre sprite sheet üzerinde hangi karenin ekrana basılacağının hesaplanmasıdır.
Sprite sheet
Animasyonun her bir karesinin ayrı bir resim dosyası olarak tutulması, daha fazla bellek kullanımına yol açar. Ayrıca dosyaların bellekte dağınık durması çalışma zamanında performansı olumsuz etkiler.
Sprite sheet (animasyon sayfası) kullanmanın mantığı, animasyonun tüm karelerinin tek büyük bir sayfada sıkıştırılıp yeri geldiğinde bu sayfanın ilgili kısımlarını ekrana basmaktır.
Sprite sheet'in anlamlı şekilde düzenlenmesi
Sprite sheet üzerindeki dizilimin kendi içinde bir anlamı olması, kod içindeki kullanımını da kolaylaştırır. Google görsellerden bulduğum ve biraz düzenlediğim aşağıdaki sprite sheet'te, yürüme animasyonu 8 farklı açı için çizilmiş ve yürüme döngüsü (iki adım) 8 kareden oluşmaktadır. Soldan sağa yürüme animasyonunun kareleri, yukarıdan aşağıya ise aynı karenin 0 dereceden 360 dereceye kadar farklı açıları görülmektedir.
Sprite sheet üzerindeki dizilimin kendi içinde bir anlamı olması, kod içindeki kullanımını da kolaylaştırır. Google görsellerden bulduğum ve biraz düzenlediğim aşağıdaki sprite sheet'te, yürüme animasyonu 8 farklı açı için çizilmiş ve yürüme döngüsü (iki adım) 8 kareden oluşmaktadır. Soldan sağa yürüme animasyonunun kareleri, yukarıdan aşağıya ise aynı karenin 0 dereceden 360 dereceye kadar farklı açıları görülmektedir.
Eskiden kaydırılan şeritli takvimler olurdu. Şeffaf bir bantın üzerinde kaydırılan kırmızı kare bir çerçeve, ayın kaçı ise o sayının üzerine getirilirdi. Biz de sprite sheet'in üzerinde render edeceğimiz kareyi hesaplamak için öyle bir çerçeve kullanacağız.
Eğer karakter yürümekte ise, kırmızı çerçeve sonsuza kadar soldan sağa doğru ilerleyecek (en sona gelince başa dönerek). Eğer karakterin açısı değişirse çerçeve dikey olarak ayarlanacak. Şekilde yürüme animasyonunun 3. karesinde olan karakter, saat yönünün tersinde 45 derece dönerse çerçeve bir birim aşağıya inip, animasyonun devam etmesi için de bir birim sağa kayacak. Böylece açısı değişse bile adımına kaldığı yerden devam etmesi de sağlanmış olacak.
Kodlama
/* Projeye göre farklılık gösterecek kısımları mümkün olduğunca buraya dahil etmemeye ve okumayı hızlandırmak için sadece ilgili yerleri comment'lemeye çalıştım */
#include <iostream>
#include <unistd.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
/* Yukarıdaki sprite sheet'e göre ayarlanmış sabitler */
const int PLAYER_SPRITE_WIDTH = 48;
const int PLAYER_SPRITE_HEIGHT = 48;
const int PLAYER_WALK_ANIMATION_FRAME_COUNT = 8;
const int PLAYER_WALK_ANIMATION_ANGLE_COUNT = 8;
const int PLAYER_VEL_X = 3;
const int PLAYER_VEL_Y = 3;
SDL_Window* window;
SDL_Renderer* renderer;
void initGraphics() {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
window = SDL_CreateWindow("İzometrik yürüme animasyonu", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
}
void closeGraphics() {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
window = NULL;
renderer = NULL;
IMG_Quit();
SDL_Quit();
}
class Player {
public:
/* Karakterin durumları */
bool isAlive;
bool isIdle, isWalking;
/* Karakterin konum ve yön bilgisi */
SDL_Point position;
int angle;
/* Yürüme animasyonunun sprite sheet'i */
SDL_Texture* walkingSpritesheet;
/* Kırmızı çerçevemiz :) */
SDL_Rect animationCursor;
Player() {
isAlive = false;
isIdle = true;
isWalking = false;
position = {-1, -1};
angle = -1;
walkingSpritesheet = load("walking.png");
animationCursor = {0,0,PLAYER_SPRITE_WIDTH,PLAYER_SPRITE_HEIGHT};
}
/* Sprite sheet'in yüklenmesi */
SDL_Texture* load(std::string path) {
SDL_Texture* loadedTexture = NULL;
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
loadedTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
SDL_free(loadedSurface);
return loadedTexture;
}
void spawn(SDL_Point spawnPoint, int facingAngle =
0) {
isAlive = true;
position = spawnPoint;
angle = facingAngle;
}
void die() {
isAlive = false;
position = {-1,-1};
angle = -1;
}
void update() {
const Uint8 *state = SDL_GetKeyboardState(NULL);
/* Karakterin hız vektörünün bileşenleri */
int velX = 0;
int velY = 0;
/* Karakterin varsayılan açısı en son hangi açıda kaldıysa o. */
int angle = this->angle;
/* Klavye girdisine göre velX ve velY'nin değerlerini buluyoruz. */
if (state[SDL_SCANCODE_UP]) {
velY = -PLAYER_VEL_Y;
}
if (state[SDL_SCANCODE_DOWN]) {
velY = +PLAYER_VEL_Y;
}
if (state[SDL_SCANCODE_LEFT]) {
velX = -PLAYER_VEL_X;
}
if (state[SDL_SCANCODE_RIGHT]) {
velX = +PLAYER_VEL_X;
}
/*
Artık karakterin Vx ve Vy değerlerini biliyoruz. Şimdi bunların bileşkesinin hangi yöne baktığını bulacağız. Eğer hız vektörü (0,0) ise karakter duruyor demektir ve açısı belirsizdir. Bu yüzden varsayılan olarak en son hangi açıda kalmış idiyse o açıda görünmesini ayarlamıştık.
Kodlama
/* Projeye göre farklılık gösterecek kısımları mümkün olduğunca buraya dahil etmemeye ve okumayı hızlandırmak için sadece ilgili yerleri comment'lemeye çalıştım */
#include <iostream>
#include <unistd.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
/* Yukarıdaki sprite sheet'e göre ayarlanmış sabitler */
const int PLAYER_SPRITE_WIDTH = 48;
const int PLAYER_SPRITE_HEIGHT = 48;
const int PLAYER_WALK_ANIMATION_FRAME_COUNT = 8;
const int PLAYER_WALK_ANIMATION_ANGLE_COUNT = 8;
const int PLAYER_VEL_X = 3;
const int PLAYER_VEL_Y = 3;
SDL_Window* window;
SDL_Renderer* renderer;
void initGraphics() {
SDL_Init(SDL_INIT_VIDEO);
IMG_Init(IMG_INIT_PNG);
window = SDL_CreateWindow("İzometrik yürüme animasyonu", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_SHOWN);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
}
void closeGraphics() {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
window = NULL;
renderer = NULL;
IMG_Quit();
SDL_Quit();
}
class Player {
public:
/* Karakterin durumları */
bool isAlive;
bool isIdle, isWalking;
/* Karakterin konum ve yön bilgisi */
SDL_Point position;
int angle;
/* Yürüme animasyonunun sprite sheet'i */
SDL_Texture* walkingSpritesheet;
/* Kırmızı çerçevemiz :) */
SDL_Rect animationCursor;
Player() {
isAlive = false;
isIdle = true;
isWalking = false;
position = {-1, -1};
angle = -1;
walkingSpritesheet = load("walking.png");
animationCursor = {0,0,PLAYER_SPRITE_WIDTH,PLAYER_SPRITE_HEIGHT};
}
/* Sprite sheet'in yüklenmesi */
SDL_Texture* load(std::string path) {
SDL_Texture* loadedTexture = NULL;
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
loadedTexture = SDL_CreateTextureFromSurface(renderer, loadedSurface);
SDL_free(loadedSurface);
return loadedTexture;
}
void spawn(SDL_Point spawnPoint, int facingAngle =
0) {
isAlive = true;
position = spawnPoint;
angle = facingAngle;
}
void die() {
isAlive = false;
position = {-1,-1};
angle = -1;
}
void update() {
const Uint8 *state = SDL_GetKeyboardState(NULL);
/* Karakterin hız vektörünün bileşenleri */
int velX = 0;
int velY = 0;
/* Karakterin varsayılan açısı en son hangi açıda kaldıysa o. */
int angle = this->angle;
/* Klavye girdisine göre velX ve velY'nin değerlerini buluyoruz. */
if (state[SDL_SCANCODE_UP]) {
velY = -PLAYER_VEL_Y;
}
if (state[SDL_SCANCODE_DOWN]) {
velY = +PLAYER_VEL_Y;
}
if (state[SDL_SCANCODE_LEFT]) {
velX = -PLAYER_VEL_X;
}
if (state[SDL_SCANCODE_RIGHT]) {
velX = +PLAYER_VEL_X;
}
/*
Artık karakterin Vx ve Vy değerlerini biliyoruz. Şimdi bunların bileşkesinin hangi yöne baktığını bulacağız. Eğer hız vektörü (0,0) ise karakter duruyor demektir ve açısı belirsizdir. Bu yüzden varsayılan olarak en son hangi açıda kalmış idiyse o açıda görünmesini ayarlamıştık.
SDL ekranında koordinat düzleminin bildiğimizden farklı olarak y ekseninin ters olduğunu hatırlayalım. */
/* Yukarıda 8 durumda karakter yürümekte, 1 durumda durmakta olduğu için varsayılanı yürüyor sayıyoruz. */
isIdle = false;
isWalking = true;
/* Aşağıdaki üç if yukarıdaki tablonun gerçekleştirimidir. ikinci if'te "durma" durumu ayarlanıyor. Bu durumda angle'ın üzerine değer yazmıyoruz, yukarıda yaptığımız atama geçerli olup önceki açısında kalıyor. */
if (velX == PLAYER_VEL_X) {
switch(velY) {
case 0 : angle = 0; break;
case PLAYER_VEL_Y : angle = 315; break;
case -PLAYER_VEL_Y : angle = 45; break;
}
} else if(velX == 0) {
switch(velY) {
case 0 : isIdle = true; isWalking = false; break;
case PLAYER_VEL_Y : angle = 270; break;
case -PLAYER_VEL_Y : angle = 90; break;
}
} else if(velX == -PLAYER_VEL_X) {
switch (velY) {
case 0 : angle = 180; break;
case PLAYER_VEL_Y : angle = 225; break;
case -PLAYER_VEL_Y : angle = 135; break;
}
}
/* Hesapladığımız değerleri karaktere uyguluyoruz. */
this->angle = angle;
this->position.x += velX;
this->position.y += velY;
/* Artık karakterimizin yürümekte olup olmadığı ve açısı belli. Bu bilgilere göre kırmızı çerçevemizi ayarlama işini render() metodunda yapacağız. */
}
void render() {
if(isWalking) {
/* Karakter yürümekte ise çerçeveyi bir kare sağa kaydır. */
animationCursor.x += PLAYER_SPRITE_WIDTH;
/* Çerçeve en sağa geldiyse en baştan devam etmesini sağla. */
animationCursor.x %= PLAYER_SPRITE_WIDTH*PLAYER_WALK_ANIMATION_FRAME_COUNT;
}
/* Karakterin açısına göre çerçeveyi dikeyde ayarla. angl/45 yukarıdaki tablodaki formülden geliyor. */
animationCursor.y = (angle/45)*PLAYER_SPRITE_HEIGHT;
//Son olarak çerçevenin iç kısmını karakterin sahnede bulunması gereken yere basıyoruz.
SDL_Rect srcRect = animationCursor;
SDL_Rect dstRect = {position.x, position.y, PLAYER_SPRITE_WIDTH, PLAYER_SPRITE_HEIGHT};
SDL_RenderCopy(renderer, walkingSpritesheet, &srcRect, &dstRect);
}
};
/* Yazdığımız kodu deniyoruz. */
int main() {
initGraphics();
Player* player;
player = new Player();
SDL_Point spawnPoint = {200,200};
player->spawn(spawnPoint, 0);
SDL_Event e;
/* Oyun döngüsü */
while (1 == 1) {
SDL_Delay(100);
player->update();
SDL_RenderClear(renderer);
player->render();
SDL_RenderPresent(renderer);
/* Oyunu kapat */
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) {
closeGraphics();
return 0;
}
}
}
}
/* walk.png'yi proje klasörüne atmayı unutmayın (Kıps ;) ) */
/* Yukarıda 8 durumda karakter yürümekte, 1 durumda durmakta olduğu için varsayılanı yürüyor sayıyoruz. */
isIdle = false;
isWalking = true;
/* Aşağıdaki üç if yukarıdaki tablonun gerçekleştirimidir. ikinci if'te "durma" durumu ayarlanıyor. Bu durumda angle'ın üzerine değer yazmıyoruz, yukarıda yaptığımız atama geçerli olup önceki açısında kalıyor. */
if (velX == PLAYER_VEL_X) {
switch(velY) {
case 0 : angle = 0; break;
case PLAYER_VEL_Y : angle = 315; break;
case -PLAYER_VEL_Y : angle = 45; break;
}
} else if(velX == 0) {
switch(velY) {
case 0 : isIdle = true; isWalking = false; break;
case PLAYER_VEL_Y : angle = 270; break;
case -PLAYER_VEL_Y : angle = 90; break;
}
} else if(velX == -PLAYER_VEL_X) {
switch (velY) {
case 0 : angle = 180; break;
case PLAYER_VEL_Y : angle = 225; break;
case -PLAYER_VEL_Y : angle = 135; break;
}
}
/* Hesapladığımız değerleri karaktere uyguluyoruz. */
this->angle = angle;
this->position.x += velX;
this->position.y += velY;
/* Artık karakterimizin yürümekte olup olmadığı ve açısı belli. Bu bilgilere göre kırmızı çerçevemizi ayarlama işini render() metodunda yapacağız. */
}
void render() {
if(isWalking) {
/* Karakter yürümekte ise çerçeveyi bir kare sağa kaydır. */
animationCursor.x += PLAYER_SPRITE_WIDTH;
/* Çerçeve en sağa geldiyse en baştan devam etmesini sağla. */
animationCursor.x %= PLAYER_SPRITE_WIDTH*PLAYER_WALK_ANIMATION_FRAME_COUNT;
}
/* Karakterin açısına göre çerçeveyi dikeyde ayarla. angl/45 yukarıdaki tablodaki formülden geliyor. */
animationCursor.y = (angle/45)*PLAYER_SPRITE_HEIGHT;
//Son olarak çerçevenin iç kısmını karakterin sahnede bulunması gereken yere basıyoruz.
SDL_Rect srcRect = animationCursor;
SDL_Rect dstRect = {position.x, position.y, PLAYER_SPRITE_WIDTH, PLAYER_SPRITE_HEIGHT};
SDL_RenderCopy(renderer, walkingSpritesheet, &srcRect, &dstRect);
}
};
/* Yazdığımız kodu deniyoruz. */
int main() {
initGraphics();
Player* player;
player = new Player();
SDL_Point spawnPoint = {200,200};
player->spawn(spawnPoint, 0);
SDL_Event e;
/* Oyun döngüsü */
while (1 == 1) {
SDL_Delay(100);
player->update();
SDL_RenderClear(renderer);
player->render();
SDL_RenderPresent(renderer);
/* Oyunu kapat */
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT) {
closeGraphics();
return 0;
}
}
}
}
/* walk.png'yi proje klasörüne atmayı unutmayın (Kıps ;) ) */