Объектно-ориентированный подход при создании игр на JavaScript

Как улучшить программный код, используя объектно-ориентированное программирование и шаблоны проектирования

Значительная часть JavaScript-кода представляет собой циклы на базе процедур и гигантские конструкции типа if/else. Из этой статьи вы узнаете о более разумном подходе на примере использования объектно-ориентированного проектирования при создании игр на языке JavaScript. Вы познакомитесь с обзором прототипного наследования и с основами объектно-ориентированного программирования (ООП) на JavaScript. Вы узнаете, как получать дополнительные преимущества от применения ООП на JavaScript посредством использования библиотек на базе классического наследования. И, наконец, в статье рассматриваются шаблоны архитектурного проектирования, которые демонстрируют написание более "чистого" кода на таких примерах, как игровой цикл, машина с конечным числом состояний и всплывающие события.

Дэн Здразил, разработчик программного обеспечения, The Nerdery

Photo of Dan ZdrazilДэн Здразил (Dan Zdrazil) начал работать с PHP в 2008 году, а затем добавил к своему портфелю Flex/AS3, Zend, JavaScript и HTML. Он работает на постоянной основе в компании The Nerdery в качестве инженера по программному обеспечению и является участником созданного в этой компании комитета по JavaScript, где регулярно делится своими знаниями со всеми, кто увлекается этой технологией. Д. Здразил окончил Миннесотский университет со степенью бакалавра искусств в области азиатских языков и литературы.



16.10.2012

Введение

В статье рассматриваются принципы объектно-ориентированного программирования (ООП) на JavaScript, а также описываются прототипная и классическая модели наследования. Приведенные в статье примеры иллюстрируют встречающиеся при создании игр типовые шаблоны, которые могут значительно выиграть от структурированности и удобства сопровождения ООП-подхода. Конечная цель состоит в том, чтобы каждый фрагмент программного кода был бы доступен для прочтения человеком, выражал бы идею разработчика и служил бы общей цели, в результате чего вся совокупность инструкций и алгоритмов превратилась бы в гармоничное произведение искусства.


Обзор ООП в JavaScript

Часто используемые сокращения

  • DOM: Document Object Model (объектная модель документа)
  • DRY: Don't Repeat Yourself (не повторяйся — принцип разработки программного обеспечения)
  • OOP: Object-oriented programming (объектно-ориентированное программирование)

Цель объектно-ориентированного программирования (ООП) состоит в том, чтобы обеспечить функционирование таких концепций, как абстракция, модульность, инкапсуляция, полиморфизм и наследование. ООП позволяет абстрагировать концепцию программного кода от его непосредственной разработки и тем самым обеспечить ему элегантность, возможность многократного использования и удобочитаемость — ценой увеличения количества файлов и количества строк кода, а также (в случае плохого управления) снижения производительности.

Традиционно разработчики игр избегали чистых ООП-подходов, что позволяло им выжимать максимально возможную производительность из каждого цикла центрального процессора. Авторы многих учебных пособий с целью быстрого создания демонстрационных версий JavaScript-игр используют не соответствующие принципам ООП подходы – вместо того, чтобы предоставить прочный фундамент. Проблемы разработчиков игр на JavaScript отличаются от проблем разработчиков для других сред: управление памятью осуществляется не в ручном режиме, а JavaScript-файлы исполняются в глобальном контексте, что порождает кошмары сопровождения в виде запутанного программного кода, конфликтов пространств имен и лабиринтоподобных конструкций if/else. Чтобы реализовать максимум возможного при разработке JavaScript-игры, применяйте наилучшие методики ООП. Это позволит значительно улучшить такие аспекты вашей игры, как удобство последующего сопровождения, скорость разработки и выразительность.

Прототипное наследование

В отличие от языков, в которых используется классическое наследование (от слова "класс"), в JavaScript встроенная конструкция класса отсутствует. В мире JavaScript "объектами первого класса" (first-class object) являются функции; как и у всех определяемых пользователем объектов, у них есть прототипы. Вызов какой-либо функции с ключевым словом new фактически создает копию объекта-прототипа этой функции и использует этот объект в качестве контекста для ключевого слова this внутри этой функции. Соответствующий пример показан в листинге 1.

Листинг 1. Конструирование объекта с помощью прототипов
// constructor function
function MyExample() {
  // property of an instance when used with the 'new' keyword
  this.isTrue = true;
};

MyExample.prototype.getTrue = function() {
  return this.isTrue;
}

MyExample();
// here, MyExample was called in the global context, 
// so the window object now has an isTrue property—this is NOT a good practice

MyExample.getTrue;
// this is undefined—the getTrue method is a part of the MyExample prototype, 
// not the function itself

var example = new MyExample();
// example is now an object whose prototype is MyExample.prototype

example.getTrue; // evaluates to a function
example.getTrue(); // evaluates to true because isTrue is a property of the 
                   // example instance

Согласно установленным нормам, функция, представляющая класс, должна начинаться с прописной буквы, чтобы тем самым показать, что она предназначена для использования в качестве конструктора. Имя функции должно отражать структуру данных, которую она создает.

Создание экземпляров классов осуществляется посредством сочетания ключевого слова new и объектов-прототипов. Объекты-прототипы могут иметь как методы, так и свойства (см. листинг 2).

Листинг 2. Простое наследование от прототипа
// Base class
function Character() {};

Character.prototype.health = 100;

Character.prototype.getHealth = function() {
  return this.health;
}

// Inherited classes

function Player() {
  this.health = 200;
}

Player.prototype = new Character;

function Monster() {}

Monster.prototype = new Character;

var player1 = new Player();

var monster1 = new Monster();

player1.getHealth(); // 200- assigned in constructor

monster1.getHealth(); // 100- inherited from the prototype object

Для присвоения родительского класса дочернему классу требуется осуществить вызов с ключевым словом new и присвоить результат свойству prototype дочернего класса (см. листинг 3). Вследствие этого рекомендуется в максимально возможной степени делать конструкторы компактными и свободными от побочных эффектов — до тех пор, пока вы не собираетесь в своих определениях классов передавать значения по умолчанию.

Если вы уже пытались определять классы и наследование на языке JavaScript, то наверняка осознали его основное отличие от классических ООП-языков: в нем отсутствуют свойства super и parent, которые позволяли бы получить доступ к методам родительского объекта в случае их переопределения. Для этой ситуации существует простое решение, однако оно нарушает принципы DRY, что, вероятно, и является основной причиной наличия столь большого количества библиотек, которые пытаются имитировать классическое наследование.

Листинг 3. Вызов методов родителя из дочерних классов
function ParentClass() {
  this.color = 'red';
  this.shape = 'square';
}

function ChildClass() {
  ParentClass.call(this);  // use 'call' or 'apply' and pass in the child 
                           // class's context
  this.shape = 'circle';
}

ChildClass.prototype = new ParentClass(); // ChildClass inherits from ParentClass

ChildClass.prototype.getColor = function() {
  return this.color; // returns "red" from the inherited property
};

В листинге 3 значения атрибутов color и shape находятся не в прототипе — они присваиваются в функции конструктора ParentClass. Свойству shape в новых экземплярах ChildClass значение присваивается два раза — один раз в конструкторе ParentClass ему присваивается значение square и один раз в конструкторе ChildClass ему присваивается значение circle. Перемещение в прототип такой логики, как эти присвоения, ослабит вышеописанные побочные эффекты и облегчит сопровождение программного кода.

В моделях прототипного наследования для исполнения функции с разным контекстом можно использовать JavaScript-методы call и apply Этот подход хорошо работает в качестве замены использованию свойств super и parent, имеющихся в других языках, однако он порождает иную проблему. Если вам необходимо осуществить рефакторинг класса, например, изменить его имя, его родителя или имя его родителя, теперь у вас есть маркер ParentClass в соответствующем текстовом файле в гораздо большем количестве мест. Эта проблема усиливается по мере того, как ваши классы становятся все более сложными. Более привлекательное решение состоит в том, чтобы ваши классы расширяли некий базовый класс, что позволило бы вашему коду повторять себя меньшее количество раз, обычно в форме воспроизведения классического наследования.

Классическое наследование

Хотя прототипное наследование полностью подходит для использования в ООП, оно не удовлетворяет нескольким критериям хорошего стиля программирования. В частности, оно имеет следующие недостатки.

  • Оно не соответствует принципам DRY. Имена классов и прототипы повторяются буквально повсюду, что затрудняет чтение и рефакторинг.
  • Вызов конструкторов осуществляется во время определения прототипа. Вы не можете использовать определенную логику в конструкторах после того, как начинаете разбиение на подклассы.
  • Отсутствует реальная поддержка сильной инкапсуляции.
  • Отсутствует реальная поддержка статических членов класса.

Многие JavaScript-библиотеки пытаются соблюдать более классический синтаксис ООП, чтобы преодолеть вышеупомянутые проблемы. Одна из таких простых в использовании библиотек — Base.js, создателем которой является Дин Эдвард (Dean Edward) (см. раздел Ресурсы). Эта библиотека предоставляет следующие полезные возможности.

  • Все прототипирование осуществляется с помощью т.н. "объектов-примесей" (object mix-ins); (классы и подклассы могут быть определены в одном утверждении).
  • Для предоставления безопасного места для логики, которая подлежит исполнению при создании новых экземпляров класса, используется специальная функция конструктора.
  • Тем самым реализуется поддержка статических членов класса.
  • Вклад этой библиотеки в сильную инкапсуляцию кончается на минимизации определения класса до одного утверждения (инкапсуляция мысли вместо инкапсуляции кода).

Другие библиотеки могут предлагать более строгую поддержку публичных и частных методов и свойств (инкапсуляция), однако библиотека Base.js предоставляет краткий синтаксис, простой в использовании и легкий для запоминания.

Листинг 4 представляет собой краткое введение в Base.js и в классическое наследование. В этом примере характеристики абстрактного класса Enemy расширяются посредством более определенного специфического класса RobotEnemy.

Листинг 4. Краткое введение в Base.js и в классическое наследование
// create an abstract, basic class for all enemies
// the object used in the .extend() method is the prototype
var Enemy = Base.extend({
    health: 0,
    damage: 0,
    isEnemy: true,

    constructor: function() {
        // this is called every time you use "new"
    },

    attack: function(player) {
        player.hit(this.damage); // "this" is your enemy!
    }
});

// create a robot class that uses Enemy as its parent
// 
var RobotEnemy = Enemy.extend({
    health: 100,
    damage: 10,

    // because a constructor isn't listed here, 
    // Base.js automatically uses the Enemy constructor for us

    attack: function(player) {
        // you can call methods from the parent class using this.base
        // by not having to refer to the parent class
        // or use call / apply, refactoring is easier
        // in this example, the player will be hit
        this.base(player); 
       
        // even though you used the parent class's "attack" 
        // method, you can still have logic specific to your robot class
        this.health += 10;
    }
});

ООП-шаблоны при проектировании игр

Базовый механизм игры неизменно опирается на следующие две функции: update и render. В свою очередь, метод render обычно использует метод setInterval или polyfill-скрипт для API-интерфейса requestAnimationFrame, напр., созданный Полом Айришем (Paul Irish) (см. раздел Ресурсы). Преимущество использования интерфейса requestAnimationFrame состоит в том, что его вызов осуществляется не чаще, чем это необходимо. Он будет исполняться лишь с частотой обновления монитора пользователя (как правило, 60 раз в секунду для настольных систем), а в большинстве браузеров он вообще не будет исполняться, если вкладка, в которой находится игра, не является активной. Преимущества:

  • Сокращение объема выполняемой клиентским компьютером работы в то время, когда пользователь не смотрит на игру.
  • Увеличение времени работы от аккумулятора на мобильных устройствах.
  • Эффективный механизм приостановки игры, если ее цикл обновления (update) связан с циклом рендеринга (render).

По вышеуказанным причинам применение requestAnimationFrame считается дружественным к клиенту и более предпочтительным по сравнению с setInterval.

Связывание цикла update с циклом render порождает другую проблему: поддержание одинаковой частоты для игровых действий и для анимаций независимо от того, осуществляется ли цикл рендеринга с частотой 15 или 60 кадров в секунду. Трюк для преодоления этой проблемы состоит в том, чтобы внутри игры ввести единицу времени под названием такт (tick) и передавать количество времени, прошедшего с момента последнего вызова до обновления. Затем это количество времени может быть преобразовано в количество тактов, после чего модели, физические механизмы и другая зависящая от времени игровая логика будут настроены соответствующим образом. Например, отравленный игрок может получать по 10 баллов ущерба на каждом такте на протяжении 10 тактов. Если цикл рендеринга работает быстро, то до момента обновления определенного вызова ущерб может вообще не наступить. Однако если на последнем цикле рендеринга будет активирована сборка мусора, вызывающая пропуск полутора тактов, то игровая логика может, напротив, присвоить игроку ущерб величиной 15 баллов.

Другой подход состоит в полном отделении синхронизации обновлений модели от цикла рендеринга изображения. В играх, имеющих большое количество анимаций или объектов, либо по тем или иным причинам тратящих большое количество ресурсов на рисование, связывание цикла обновления с циклом рендеринга приведет к общему замедлению игры. В этом случае метод update может работать на интервале установки (с использованием метода setInterval) независимо от того, когда и как часто запускается обработчик requestAnimationFrame. Большая часть времени, расходуемого в этих циклах, фактически расходуется на шаге рендеринга, поэтому игра продолжит функционирование с заданной скоростью, даже если рисование на экране осуществляется с частотой всего лишь 25 кадров/с. В обоих случаях разработчику по-прежнему необходимо вычислять разницу во времени между циклами обновления; если обновление производится 60 раз в секунду, то у функции обновления имеется до завершения не более 16 мс. Если эта функция будет исполняться дольше (или если в браузере будет запущена сборка мусора), то игра будет по-прежнему замедляться. Соответствующий пример показан в листинге 5.

Листинг 5. Базовый класс приложения с циклами render и update loops
// requestAnim shim layer by Paul Irish
    window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       || 
              window.webkitRequestAnimationFrame || 
              window.mozRequestAnimationFrame    || 
              window.oRequestAnimationFrame      || 
              window.msRequestAnimationFrame     || 
              function(/* function */ callback, /* DOMElement */ element){
                window.setTimeout(callback, 1000 / 60);
              };
    })();

var Engine = Base.extend({
    stateMachine: null,  // state machine that handles state transitions
    viewStack: null,     // array collection of view layers, 
                         // perhaps including sub-view classes
    entities: null,      // array collection of active entities within the system
                         // characters, 
    constructor: function() {
        this.viewStack = []; // don't forget that arrays shouldn't be prototype 
		                     // properties as they're copied by reference
        this.entities = [];

        // set up your state machine here, along with the current state
        // this will be expanded upon in the next section

        // start rendering your views
        this.render();
       // start updating any entities that may exist
       setInterval(this.update.bind(this), Engine.UPDATE_INTERVAL);
    },

    render: function() {
        requestAnimFrame(this.render.bind(this));
        for (var i = 0, len = this.viewStack.length; i < len; i++) {
            // delegate rendering logic to each view layer
            (this.viewStack[i]).render();
        }
    },

    update: function() {
        for (var i = 0, len = this.entities.length; i < len; i++) {
            // delegate update logic to each entity
            (this.entities[i]).update();
        }
    }
}, 

// Syntax for Class "Static" properties in Base.js. Pass in as an optional
// second argument to.extend()
{
    UPDATE_INTERVAL: 1000 / 16
});

Если вы не знакомы с контекстом this в JavaScript, то обратите внимание, что .bind(this) используется дважды — один раз в анонимной функции внутри вызова setInterval и один раз в this.render.bind() внутри вызова requestAnimFrame. В данном случае setInterval и requestAnimFrame— это функции, а не методы; они принадлежат глобальному объекту window, а не какому-либо определенному классу или сущности. В результате для того, чтобы this внутри методов render и update игрового механизма ссылался на наш экземпляр класса Engine, вызов .bind(object) вынуждает this внутри функции действовать не так, как обычно. При необходимости поддержки браузера Internet Explorer версии 8 или ниже необходимо добавить соответствующий polyfill-скрипт для реализации связывания.


Шаблон State Machine

Шаблон State Machine (state machine — машина с конечным числом состояний) широко применяется, хотя это не всегда осознается. Данный шаблон представляет собой расширение принципов ООП (абстрагирование концепции программного кода от его исполнения). Например, игра может иметь следующие состояния:

  • Предварительная загрузка
  • Начальный экран
  • Активная игра
  • Меню опций
  • Конец игры (победа, проигрыш, продолжение)

Ни в одном из этих состояний не должно быть исполняемого кода, зависящего от других состояний. Например, программный код предварительной загрузки ничего не должен знать о том, когда следует открывать меню опций. Императивное (процедурное) программирование могло бы предложить монолитные условные операторы if или switch, чтобы надлежащим образом упорядочить логику приложения, однако такие операторы не в состоянии представить концепцию программного кода, что усложняет последующее сопровождение. Введение дополнительных состояний, таких как внутриигровое меню, переходы между уровнями, новые функции и т.д., в еще большей степени усложняет сопровождение условных операторов.

Вместо этого рассмотрим пример в листинге 6.

Листинг 6. Упрощенная машина с конечным числом состояний
// State Machine
var StateMachine = Base.extend({
    states: null, // this will be an array, but avoid arrays on prototypes.
                  // as they're shared across all instances!
    currentState: null, // may or may not be set in constructor
    constructor: function(options) {
        options = options || {}; // optionally include states or contextual awareness

        this.currentState = null;
        this.states = {};

        if (options.states) {
            this.states = options.states;
        }

        if (options.currentState) {
            this.transition(options.currentState);
        }
    },

    addState: function(name, stateInstance) {
        this.states[name] = stateInstance;
    },

    // This is the most important function—it allows programmatically driven
    // changes in state, such as calling myStateMachine.transition("gameOver")
    transition: function(nextState) {
        if (this.currentState) {
            // leave the current state—transition out, unload assets, views, so on
            this.currentState.onLeave();
        }
        // change the reference to the desired state
        this.currentState = this.states[nextState];
        // enter the new state, swap in views, 
        // setup event handlers, animated transitions
        this.currentState.onEnter();
    }
});

// Abstract single state
var State = Base.extend({
    name: '',       // unique identifier used for transitions
    context: null,  // state identity context- determining state transition logic

    constructor: function(context) {
        this.context = context;
    },

    onEnter: function() {
        // abstract

        // use for transition effects
    },

    onLeave: function() {
        // abstract

        // use for transition effects and/or
        // memory management- call a destructor method to clean up object
        // references that the garbage collector might not think are ready, 
        // such as cyclical references between objects and arrays that 
        // contain the objects
    }
});

Возможно, у вас нет необходимости для своего приложения создавать специальный подкласс класса state machine, однако вы наверняка захотите создать подклассы State для каждого из состояний своего приложения. Разделение логики переходов на различные объекты позволяет:

  • Использовать конструкторы в качестве возможности для немедленного начала предварительной загрузки активов.
  • Добавлять к игре новые состояния (например, экран продолжения, который появляется перед экраном окончания игры) без необходимости выяснять, на какие глобальные переменные влияют те или иные условные выражения в тех или иных монолитных структурах if/else или switch.
  • Динамически определять логику переходов в случае создания состояний на основе данных, загружаемых с сервера.

Основной класс приложений не должен беспокоиться о логике внутри состояний, а эти состояния не должны слишком беспокоиться об основном классе приложения. Например, состояние предварительной загрузки может отвечать за формирование представления на основе активов, встроенных в разметку страницы, и за формирование очередей минимально необходимых игровых активов (видеоклипы, изображения и звуки) в диспетчере singleton-активов. Хотя это состояние задействует класс предварительной загрузки представления, оно не обязано заботиться о поведении этого представления. В данном случае идея (объект, представляемый состоянием) состоит в ограничении ответственности, –необходимо лишь определить, как должно вести себя приложение в состоянии предварительной загрузки данных.

Следует иметь в виду, что шаблон state machine не ограничен состояниями игровой логики. Отдельные представления также выиграют от исключения логики состояния из логики представления, особенно при управлении субпредставлениями или при объединении с шаблоном Chain of Responsibility с целью обработки событий, возникающих при взаимодействии с пользователем.


Шаблон Chain of Responsibility: эмуляция всплывающих событий на элементе canvas

HTML5-элемент canvas можно рассматривать как элемент изображения, который позволяет манипулировать отдельными пикселями. Например, если в какой-либо области экрана нарисована трава, лежащая на ней награбленная добыча и стоящий на всем этом персонаж, то canvas не сможет догадаться, по какому их этих предметов нажал пользователь. Если вы нарисовали меню, то canvas не знает, что определенная его область представляет собой кнопку, и что единственный DOM-элемент, с которым может быть связано событие, и есть сам canvas. Чтобы в игру действительно можно было бы играть, игровой механизм должен понимать, что может происходить при нажатии пользователя на canvas.

Шаблон проектирования Chain of Responsibility призван отделить отправителя события (DOM-элемент) от получателя (разрабатываемый код), чтобы ответственность за обработку события мог бы взять на себя более чем один объект (представления и модели). В классических реализациях, таких как веб-страницы, представления и модели могут иметь обрабатывающий интерфейс и с его помощью делегировать все порождаемые нажатиями мыши события графу сцены, который после этого находит соответствующие "вещи", на которые нажал пользователь, и предоставляет каждой из них шанс на прерывание. Более простой подход состоит в том, что на самом элементе canvas разместить цепочку обработчиков, определяемых в процессе исполнения (см. листинг 7).

Листинг 7. Обработка всплывающих событий с использованием шаблона Chain of Responsibility
var ChainOfResponsibility = Base.extend({
        context: null,      // relevant context- view, application state, so on
        handlers: null,     // array of responsibility handlers
        canPropagate: true, // whether or not 

        constructor: function(context, arrHandlers) {
            this.context = context;
            if (arrHandlers) {
                this.handlers = arrHandlers;
            } else {
                this.handlers = [];
            }
        },

        execute: function(data) {
            for (var i = 0, len = this.handlers.length; i < len; i++) {
                if (this.canPropagate) {
                    // give a handler a chance to claim responsibility
                    (this.handlers[i]).execute(this, data);
                } else {
                    // an event has claimed responsibility, no need to continue
                    break;
                } 
            }
            // reset state after event has been handled
            this.canPropagate = true;
        },

        // this is the method a handler can call to claim responsibility
        // and prevent other handlers from acting on the event
        stopPropagation: function() {
            this.canPropagate = false;
        },

        addHandler: function(handler) {
            this.handlers.push(handler);
        }
});

var ResponsibilityHandler = Base.extend({
    execute: function(chain, data) {

        // use chain to call chain.stopPropegation() if this handler claims
        // responsibility, or to get access to the chain's context member property
        // if this event handler doesn't need to claim responsibility, simply
        // return; and the next handler will execute
    }
});

Класс ChainOfResponsibility будет прекрасно работать и без разделения на подклассы, поскольку вся специфическая логика приложения будет содержаться в подклассах ResponsibilityHandler. Единственное различие между реализациями состоит в передаче внутрь соответствующего контекста, например, отображения, представляемого этим контекстом. Рассмотрим меню опций, при открытии которого приостановленная игра по-прежнему демонстрируется на экране (см. листинг 8). Если пользователь нажимает на какую-либо из кнопок этого меню, персонажи на заднем плане не должны реагировать на это нажатие.

Листинг 8. Обработчик закрытия меню опций
var OptionsMenuCloseHandler = ResponsibilityHandler.extend({
    execute: function(chain, eventData) {
        if (chain.context.isPointInBackground(eventData)) {
            // the user clicked the transparent background of our menu
            chain.context.close(); // delegate changing state to the view
            chain.stopPropegation(); // the view has closed, the event has been handled
        }
    }
});

// OptionMenuState
// Our main view class has its own states, each of which handles
// which chains of responsibility are active at any time as well
// as visual transitions

// Class definition...
constructor: function() {
    // ...
    this.chain = new ChainOfResponsibility(
        this.optionsMenuView, // the chain's context for handling responsibility
        [
            new OptionsMenuCloseHandler(), // concrete implementation of 
			                               // a ResponsibilityHandler
            // ...other responsibility handlers...
        ]
    );
}

// ...
onEnter: function() {
    // change the view's chain of responsibility
    // guarantees only the relevant code can execute
    // other states will have different chains to handle clicks on the same view
    this.context.setClickHandlerChain(this.chain);
}
// ...

В листинге 8 класс view имеет ссылку на набор состояний, а каждое состояние определяет, какие объекты будут отвечать за конкретную обработку события нажатия. В результате логика представления является ограниченной — в ее распоряжении имеется только то, что представляют ее сущности, поэтому оно отображает только меню опций. Если в результате обновления в игре появляются дополнительные кнопки, новые эффекты или переходы к новым представлениям, то для каждой новой "особенности" появляется отдельный объект, способный обрабатывать эту особенность без какой-либо необходимости изменять, нарушать или переписывать существующую логику. Разумное сочетание цепочек для таких событий, как перемещения и нажатия мыши, позволяет осуществлять обработку буквально всем — от меню и символов до перетаскивания экранов с "имуществом" — в высшей степени структурированным и организованным образом без усложнения программного кода.


Заключение

По своей сути концепции шаблонов проектирования и объектно-ориентированного программирования (ООП) являются нейтральными; их непродуманное применение способно порождать новые проблемы, вместо того чтобы решать существующие. В данной статье были рассмотрены принципы ООП при использовании JavaScript, а также описаны прототипная и классическая модели наследования. Вы узнали о широко применяемых при создании игр шаблонах, способных извлечь существенную выгоду из структурированности и удобства сопровождения ООП-дизайна (базовый игровой цикл, машина с конечным числом состояний и всплывающие события). Эта статья коснулась лишь поверхности типовых решений для типичных проблем. После небольшой практики вы станете специалистом по написанию выразительного кода — и, соответственно, будете тратить меньше времени на написание и больше времени на творчество.


Загрузка

ОписаниеИмяРазмер
Исходный код к статьеObject-OrientedDesignSource.zip5 КБ

Ресурсы

Научиться

Получить продукты и технологии

  • OpenGL: Получите новейшие драйверы.
  • jQuery: Загрузите эту популярную JavaScript-библиотеку, которая упрощает обход HTML-документов, обработку событий, анимацию и Ajax-взаимодействия с целью ускорения веб-разработки.
  • Modernizr: Загрузите эту JavaScript-библиотеку с открытым исходным кодом, которая поможет вам создавать веб-сайты следующего поколения на базе HTML5 и CSS3.
  • Kibo: Загрузите эту популярную библиотеку, специально написанную для быстрой обработки клавиатурных событий вне зависимости от используемого браузера.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура
ArticleID=943818
ArticleTitle=Объектно-ориентированный подход при создании игр на JavaScript
publish-date=10162012