Wie ich mit React.js ein Android-Handyspiel entwickelt und in den Google Play Store gestellt habe

In diesem Artikel werden alle Entwicklungsstufen betrachtet: Von der Konzeption der Idee bis zur Implementierung einzelner Teile der Anwendung, einschließlich selektiver Bereitstellung einiger benutzerdefinierter Codeteile.





Dieser Artikel kann für diejenigen nützlich sein, die gerade nachdenken oder anfangen, Spiele oder mobile Anwendungen zu entwickeln.





Screenshot des fertigen Spiels
Screenshot des fertigen Spiels

- , .





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





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





, . " " , JavaScript React, , .





. , . -, .





Die Startposition des Spielers, an der Sie die isometrische Welt sowie mögliche Bewegungsrichtungen beobachten können
, ,

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





Spielen Sie, bevor Sie nach grafischen Elementen suchen

- , . , :





Hubschrauber 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');
      
      



Fenster mit den Spieleinstellungen

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 Besonderer Dank geht an den Musiker Anton Zvarych für die Bereitstellung von Hintergrundmusik.








All Articles