Comment j'ai développé un jeu mobile Android en utilisant React.js et le mettre sur le Google Play Store

Dans cet article, nous examinerons toutes les étapes de développement: de la conception de l'idée à la mise en œuvre des différentes parties de l'application, y compris sélectivement certains morceaux de code personnalisés seront fournis.





Cet article peut être utile pour ceux qui réfléchissent ou commencent à développer des jeux ou des applications mobiles.





Capture d'écran du jeu terminé
Capture d'écran du jeu terminé

- , .





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





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





, . " " , JavaScript React, , .





. , . -, .





La position de départ du joueur, où vous pouvez observer le monde isométrique, ainsi que les directions de mouvement possibles
, ,

. 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-. , - . :





Jouez avant de rechercher des éléments graphiques

- , . , :





Hélicoptère 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');
      
      



Fenêtre des paramètres du jeu

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 Un merci spécial au musicien Anton Zvarych pour sa musique de fond.








All Articles