React ile Nasıl Oyun Geliştirilir?

6 min read

Popüler web frameworklerinden olan React.js ile doğasına aykırı olsa bile web tabanlı oyunlar geliştirebiliriz ve de mobil oyunlar..

Baslangic

Kullanılacaklar ‘PixiJS, React-Pixi, React ve Redux’tan oluşuyor. Özetle, PixiJS WebGL destekleyen web tabanlı oyun motoru, ReactPixi, PixiJS komponentlerini React uygulamamızda kullanmamıza yarayan ortam, Redux oyundaki aksiyonları kontrol etmemizi sağlayan state management paketi, React ise web uygulamalar geliştirmek için kullanılan bir javascript framework’ü.

PixiJS komponentleri

React tabanlı oyunumuzda kullanılabilecek olan PixiJS komponentleri; AnimatedSprite, BitmapText, Container, Graphics, NineSlicePlane, ParticleContainer, SimpleMesh, SimpleRope, Sprite, Text, TillingSprite.

Ayrıca useApp ve useTick hook’u var.

Örnek yaptığım React tabanlı oyunda Sprite, Text komponentlerini ve useTick hook’u kullandım. Ayrıca React Hooks ve Redux paketlerini kullandım.

import React from "react";
import ReactDOM from "react-dom";
import { Stage, AppConsumer } from "@inlet/react-pixi";
import { Provider } from "react-redux";
import GameZone from "./components/GameZone";
import store from "./reduxlayer/store";

const App = () => {
  return (
    <Stage width={500} height={500} options={{ backgroundColor: 0xe26c52 }}>
      <Provider store={store}>
        <AppConsumer>{(app) => <GameZone app={app} />}</AppConsumer>
      </Provider>
    </Stage>
  );
};

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);

Ardından oyun içi event(olay)’ları Redux action(aksiyon)’lar üzerinden tanımlıyoruz.

export const startGame = () => ({
  type: "START_GAME",
});

export const throwChar = (characterPosition, droppedPosition) => ({
  type: "THROW_CHARACTER",
  characterPosition: characterPosition,
  droppedPosition: droppedPosition,
});

export const stopChar = () => ({
  type: "STOP_CHARACTER",
});

export const moveChar = (iteration) => ({
  type: "MOVE_CHAR",
  iteration: iteration,
});

Reducer fonksiyonumuzdaki oyun içi state’i tetiklenen Redux action aracılığı ile güncelleyip oyun içi state yönetimini yapmış oluyoruz.

import * as helpers from "../../utils/export";
import * as images from "../../images/export";

const initialState = {
  character: helpers.character,
  donut: helpers.donut,
  game: helpers.game,
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "THROW_CHARACTER":
      return {
        ...state,
        character: {
          ...state.character,
          isMove: true,
          distanceDropped: helpers.distance(
            action.characterPosition,
            action.droppedPosition,
            1 / 4.5
          ),
          dirX:
            action.characterPosition.x - action.droppedPosition.x > 0 ? 1 : -1,
          dirY:
            action.characterPosition.y - action.droppedPosition.y > 0 ? 1 : -1,
          image: images.catMove,
        },
      };

    case "STOP_CHARACTER":
      return {
        ...state,
        character: {
          ...state.character,
          isMove: false,
          distanceDropped: undefined,
        },
      };

    case "MOVE_CHAR":
      if (action.iteration < state.character.distanceDropped) {
        return {
          ...state,
          character: {
            ...state.character,
            position: {
              x:
                state.character.position.x +
                Math.sin(action.iteration / 3) * state.character.dirX,
              y:
                state.character.position.y +
                Math.sin(action.iteration / 3) * state.character.dirY,
            },
            rotation: (Math.sin(action.iteration) * Math.PI) / 30,
          },
        };
      } else
        return {
          ...state,
        };

    case "EAT_SUCCESS":
      return {
        ...state,
        donut: {
          ...state.donut,
          position: helpers.randomlyXY(),
        },
        game: { ...state.game, score: state.game.score + 10 },
        character: {
          ...state.character,
          isMove: false,
          distanceDropped: undefined,
          image: images.catHappy,
          level:
            state.game.score > 0 && (state.game.score + 10) % 50 === 0
              ? state.character.level + 1
              : state.character.level,
          scale:
            state.game.score > 0 && (state.game.score + 10) % 50 === 0
              ? (state.character.scale += 0.125)
              : state.character.scale,
          nameMarginTop:
            state.game.score > 0 && (state.game.score + 10) % 50 === 0
              ? state.character.nameMarginTop + 4
              : state.character.nameMarginTop,
        },
      };

    case "START_GAME":
      return {
        ...state,
        game: { ...state.game, isStart: true },
      };

    default:
      return state;
  }
};

export default reducer;

Bazı durumlarda gerekli middleware(ara katman)’ler ile bu örnekte kullandığım gibi, “MOVE_CHAR” aksiyonu sonrası gerçekleşebilecek olan hedefi vurma “EAT_SUCCESS” veya hedefi vuramama “EAT_FAILED” aksiyonlarının tetiklenmesi ile oyun içi gerekli durumları kontrol etmiş oluyoruz.

import { put, takeLatest, all, fork, select } from "redux-saga/effects";
import { distance } from "../../utils/helpers";

const getCharacter = (state) => state.character;
const getDonut = (state) => state.donut;

function* fetchHits(action) {
  try {
    const character = yield select(getCharacter);
    const donut = yield select(getDonut);

    // if character hit target
    if (distance(character.position, donut.position, 1 / 4) < 6) {
      yield put({ type: "EAT_SUCCESS" });
    } else {
      throw new Error("Could not eat donut");
    }
  } catch (e) {
    yield put({
      type: "EAT_FAILED",
      message: e.message,
    });
  }
}

// redux actions and its trigger saga functions
function* watchHits() {
  yield takeLatest("MOVE_CHAR", fetchHits);
}

// used fork for executing functions at the same time
export default function* rootSaga() {
  yield all([fork(watchHits)]);
}

Karakter üzerinde mouseUpOutside event’i ile örnek oyun çıktısı şu şekilde,

Oyunda fazla detay olmasa da sürükle bırak mesafesine göre karakteri fırlatıp, hedefi vurdukça seviye atlama ve karakterin fiziksel olarak büyümesi gibi temel işlemler yukarıda yer alan kod mantığı ile yapılıyor.

Tavsiyeler

React’ın fonksiyonel komponent kullanım tarzı, tanımlayıcı yazım stili ve basit bir oyun fikri ile ReactPixi komponentlerinden uygun olanları kullanarak, oyun içi aksiyonları belirleyerek ve Redux ile oyun içi state’i yönetip web tabanlı oyun geliştirilebilir. Ama öncesinde React + Redux veya React +React Hooks ikililerinden birine aşina olmanız gerekiyor.

Aşağıdaki linklerden gerekli tutorial’lere ulaşabilirsiniz.

Redux, Redux-saga, ReactPixi, React Hooks

Ozet

Makaleyi yazmadan önce React-Pixi paketi ve bu paketin indirme sayısı ve kullanılması dikkatimi çekti, ben de bir deneyimlemek istedim.

Elbette ki React ile oyun programlamak oyun içi çok fazla element ve karmaşıklık ile pek mantıklı bir seçenek değil çünkü React temelde ReactDOM ile fonksiyonel HTML sayfaları yazıp web uygulamalar yapmamız için var.

Ancak yine de React + Pixi + Redux / React hooks üçlüsünü kullanan az değil. Github repo’sunda şuan 844 yıldızı var.

Örnek yaptığım oyunu Link üzerinden deneyimleyebilir, eksik gördüğünüz kısımları belirtebilir ve proje üzerinde pull request atıp kod üzerinde katkı sağlayabilirsiniz.

Oyunun kaynak kodları da şu adresteki Github repo’sunda. İnceleyip yıldızlarsanız mutlu olurum.

https://github.com/ozgur-can/react-pixi-game-EatDonut

Bu ve benzeri diğer yazılara, içeriklere veya bana ulaşmak için;