Bagaimana saya mengembangkan game seluler Android menggunakan React.js dan menaruhnya di Google Play Store

Pada artikel ini, kami akan mempertimbangkan semua tahap pengembangan: dari konsepsi ide hingga implementasi bagian individu dari aplikasi, termasuk secara selektif beberapa potongan kode khusus akan disediakan.





Artikel ini dapat bermanfaat bagi mereka yang baru berpikir atau mulai mengembangkan game atau aplikasi seluler.





Tangkapan layar dari game yang sudah selesai
Tangkapan layar dari game yang sudah selesai

- , .





, . , , , , "" , - , .





( ) , -, 8+ . - , . , - , - , . , , .





, . " " , JavaScript React, , .





. , . -, .





Posisi awal pemain, di mana Anda dapat mengamati dunia isometrik, serta kemungkinan arah pergerakan
, ,

. 64x64 . , :





.rotate {
  transform: rotateX(60deg) rotateZ(45deg);
  transform-origin: left top;
}
      
      



, "" , , . , , :





const cellOffsets = {};
export function getCellOffset(n) {
  if (n === 0) {
    return 0;
  }

  if (cellOffsets[n]) {
    return cellOffsets[n];
  }

  const result = 64 * (Math.floor(n / 2));

  cellOffsets[n] = result;

  return result;
}
      
      



:





import { getCellOffset } from 'libs/civilizations/helpers';

// ...
const offset = getCellOffset(columnIndex);

// ...
style={{
  transform: `translateX(${(64 * rowIndex) + (64 * columnIndex) - offset}px) translateY(${(64 * rowIndex) - offset}px)`,
}}
      
      



, . FixedSizeGrid



react-window



, . , - . / . . , .





, , png-. , - . :





Mainkan sebelum mencari elemen grafis

- , . , :





Helikopter Sprite

4 , , , react-i18next



. , 100 , , . redux



, . , , . , react-i18next



( ) .





import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import get from 'lodash/get';
import set from 'lodash/set';
import size from 'lodash/size';
import { emptyObj, EN, LANG, PROPS, langs } from 'defaults';
import { getLang } from 'reducers/global/selectors';
import en from './en';

export function getDetectedLang() {
  if (!global.navigator) {
    return EN;
  }

  let detected;
  if (size(navigator.languages)) {
    detected = navigator.languages[0];
  } else {
    detected = navigator.language;
  }

  if (detected) {
    detected = detected.substring(0, 2);

    if (langs.indexOf(detected) !== -1) {
      return detected;
    }
  }

  return EN;
}

const options = {
  lang: global.localStorage ?
    (localStorage.getItem(LANG) || getDetectedLang()) :
    getDetectedLang(),
};

const { lang: currentLang } = options;

const translations = {
  en,
};

if (!translations[currentLang]) {
  try {
    translations[currentLang] = require(`./${currentLang}`).default;
  } catch (err) {} // eslint-disable-line
}

export function setLang(lang = EN) {
  if (langs.indexOf(lang) === -1) {
    return;
  }

  if (global.localStorage) {
    localStorage.setItem(LANG, lang);
  }

  set(options, [LANG], lang);

  if (!translations[lang]) {
    try {
      translations[lang] = require(`./${lang}`).default;
    } catch (err) {} // eslint-disable-line
  }
}

const mapStateToProps = (state) => {
  return {
    lang: getLang(state),
  };
};

export function t(path) {
  const { lang = get(options, [LANG], EN) } = get(this, [PROPS], emptyObj);

  if (!translations[lang]) {
    try {
      translations[lang] = require(`./${lang}`).default;
    } catch (err) {} // eslint-disable-line
  }

  return get(translations[lang], path) || get(translations[EN], path, path);
}

function i18n(Comp) {
  class I18N extends Component {
    static propTypes = {
      lang: PropTypes.string,
    }

    static defaultProps = {
      lang: EN,
    }

    constructor(props) {
      super(props);

      this.t = t.bind(this);
    }

    componentWillUnmount() {
      this.unmounted = true;
    }

    render() {
      return (
        <Comp
          {...this.props}
          t={this.t}
        />
      );
    }
  }

  return connect(mapStateToProps)(I18N);
}

export default i18n;
      
      



:





import i18n from 'libs/i18n';

// ...
static propTypes = {
  t: PropTypes.func,
}

// ...
const { t } = this.props;

// ...
{t(['path', 'to', 'key'])}

// ...  ,   
{t('path.to.key')}

// ...
export default i18n(Comp);
      
      



Android 9 (, 8-, ) .





, , , , requestAnimationFrame



. Android 7 - - .





, requestAnimationFrame



, ( , , ):





import isFunction from 'lodash/isFunction';

let lastTime = 0;
const vendors = ['ms', 'moz', 'webkit', 'o'];
for (let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
  window.requestAnimationFrame = window[`${vendors[x]}RequestAnimationFrame`];
  window.cancelAnimationFrame = window[`${vendors[x]}CancelAnimationFrame`] || window[`${vendors[x]}CancelRequestAnimationFrame`];
}

if (!window.requestAnimationFrame) {
  window.requestAnimationFrame = (callback) => {
    const currTime = new Date().getTime();
    const timeToCall = Math.max(0, 16 - (currTime - lastTime));
    const id = window.setTimeout(() => { callback(currTime + timeToCall); },
      timeToCall);
    lastTime = currTime + timeToCall;
    return id;
  };
}

if (!window.cancelAnimationFrame) {
  window.cancelAnimationFrame = (id) => {
    clearTimeout(id);
  };
}

let lastFrame = null;
let raf = null;

const callbacks = [];

const loop = (now) => {
  raf = requestAnimationFrame(loop);

  const deltaT = now - lastFrame;
  // do not render frame when deltaT is too high
  if (deltaT < 160) {
    let callbacksLength = callbacks.length;
    while (callbacksLength-- > 0) {
      callbacks[callbacksLength](now);
    }
  }

  lastFrame = now;
};

export function registerRafCallback(callback) {
  if (!isFunction(callback)) {
    return;
  }

  const index = callbacks.indexOf(callback);

  // remove already existing the same callback
  if (index !== -1) {
    callbacks.splice(index, 1);
  }

  callbacks.push(callback);

  if (!raf) {
    raf = requestAnimationFrame(loop);
  }
}

export function unregisterRafCallback(callback) {
  const index = callbacks.indexOf(callback);

  if (index !== -1) {
    callbacks.splice(index, 1);
  }

  if (callbacks.length === 0 && raf) {
    cancelAnimationFrame(raf);
    raf = null;
  }
}
      
      



:





import { registerRafCallback, unregisterRafCallback } from 'client/libs/raf';

// ...
registerRafCallback(this.cooldown);

// ...
componentWillUnmount() {
  unregisterRafCallback(this.cooldown);
}
      
      



Lobby



, websocket- , websocket-, , , primus



. , npm primus-client



. save



.





:





- , . - ( - ):





import { SOUND_VOLUME } from 'defaults';

const Sound = {
  audio: null,
  volume: localStorage.getItem(SOUND_VOLUME) || 0.8,
  play(path) {
    const audio = new Audio(path);

    audio.volume = Sound.volume;

    if (Sound.audio) {
      Sound.audio.pause();
    }

    audio.play();

    Sound.audio = audio;
  },
};

export function getVolume() {
  return Sound.volume;
}

export function setVolume(volume) {
  Sound.volume = volume;

  localStorage.setItem(SOUND_VOLUME, volume);
}

export default Sound;
      
      



:





import Sound from 'client/libs/sound';

// ...
Sound.play('/mp3/win.mp3');
      
      



Jendela pengaturan game

web- . , , Cordova file://



, :





const replace = require('replace-in-file');
const path = require('path');

const options = {
  files: [
    path.resolve(__dirname, './app/*.css'),
    path.resolve(__dirname, './app/*.js'),
    path.resolve(__dirname, './app/index.html'),
  ],
  from: [/url\(\/img/g, /href="\//g, /src="\//g, /"\/mp3/g],
  to: ['url(./img', 'href="./', 'src="./', '"./mp3'],
};

replace(options)
  .then((results) => {
    console.log('Replacement results:', results);
  })
  .catch((error) => {
    console.error('Error occurred:', error);
  });
      
      



, Google Play Store , . - 46, , . , . , .





, , :





















, , Unity, tactical rts.





?

. - Google Play Store.





PS Terima kasih khusus kepada musisi Anton Zvarych karena menyediakan musik latar.








All Articles