Object-Oriented Design in JavaScript games-

Source: Internet
Author: User
In this article, you will learn about OOP in JavaScript to explore prototype inheritance models and classic inheritance models. An example shows a model in the game that can benefit a lot from the structure and maintainability of the OOP design. Our ultimate goal is to make every piece of code human-readable, and ...,. Introduction


In this article, you will learn about OOP in JavaScript to explore prototype inheritance models and classic inheritance models. An example shows a model in the game that can benefit a lot from the structure and maintainability of the OOP design. Our ultimate goal is to make every piece of code a human-readable code and represent an idea and a purpose. The combination of these codes goes beyond the set of commands and algorithms, become an exquisite work of art.


Overview of OPP in JavaScript


The goal of OOP is to provide data abstraction, modularity, encapsulation, polymorphism, and inheritance. With OOP, You can abstract the concept of code in code writing to provide elegant, reusable, and readable code, but this consumes file counts, row counts, and performance (if poorly managed ).


In the past, game developers often avoided the pure OOP method to fully utilize the performance of the CPU cycle. Many JavaScript game tutorials use a non-OOP method, hoping to provide a quick demonstration, rather than a solid foundation. Compared with other game developers, JavaScript developers face different problems: memory is not manually managed, and JavaScript files are executed in a global context, clusive code, namespace conflicts, and a maze of if/else statements may cause a nightmare of maintainability. To maximize the benefits of JavaScript game development, follow the best OOP practices to significantly improve future maintainability, development progress, and game performance.


Prototype inheritance


Unlike the language inherited by the classic, there is no built-in class structure in JavaScript. Functions are the first-level citizens of the JavaScript world, and they also have prototypes similar to all user-defined objects. Calling a function with the new Keyword actually creates a copy of the prototype object of the function and uses the object as the context of the keyword "this" in the function. Listing 1 provides an example.


Listing 1. Build an object with a prototype



// 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
// Example instance


By convention, a function representing a class should start with an uppercase letter, which indicates that it is a constructor. This name should be able to represent the data structure it created.


The secret to creating a class instance is to combine new keywords and prototype objects. The prototype object can have both methods and properties, as shown in Listing 2.


Listing 2. Simple inheritance through prototype


// 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


To assign a parent class to a subclass, call new and assign the result to the prototype attribute of the subclass, as shown in listing 3. Therefore, it is wise to keep the constructor as concise as possible without any side effects unless you want to pass the default value in the class definition.


If you have begun to define classes and inheritance in JavaScript, you may be aware of an important difference between the language and the classical OOP language: If you have covered these methods, no super or parent attribute can be used to access the parent object. There is a simple solution for this, but this solution violates the "do not repeat yourself (DRY)" principle, it is likely that there are many libraries trying to imitate the most important reason for classic inheritance.


Listing 3. Calling the parent method from a subclass


Function ParentClass (){
This. color = 'red ';
This. shape = 'square ';
}

Function ChildClass (){
ParentClass. call (this); // use 'call' or 'application' 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
};


In listing 3, the color and shape attributes are not in the prototype, and they are assigned values in the ParentClass constructor. The new ChildClass instance will assign two values to its shape attributes: "squre" in the ParentClass constructor and "circle" in the ChildClass constructor ". Moving the logic similar to these values to the prototype reduces side effects and makes the code easier to maintain.


In the prototype inheritance model, you can use the call or apply method of JavaScript to run functions with different contexts. Although this approach is very effective and can replace super or parent in other languages, it brings new problems. If you need to reconstruct a class by changing its name, its parent class, or its parent class name, this ParentClass is now available in many parts of your text file. As your classes become more complex, such problems will also grow. A better solution is to extend your class to a base class to reduce code duplication, especially when classic inheritance is re-created.


Classic inheritance


Although prototype inheritance is completely feasible for OOP, it cannot meet some of the goals of excellent programming. For example:


  • It is not DRY. The class name and prototype are repeated everywhere, making reading and refactoring more difficult.
  • The constructor is called during prototyping. Once subclass is started, some logic in the constructor cannot be used.
  • There is no real support for strong encapsulation.
  • No real support is provided for static class members.


Many JavaScript libraries try to implement more classical OOP syntax to solve the above problem. One of the easier-to-use libraries is Dean Edward's Base. js, which provides the following useful features:


  • All prototype operations are completed by combining objects (classes and subclasses can be defined in a statement.
  • A special constructor provides a security place for the logic that runs when a new class instance is created.
  • It supports static class members.
  • Its contribution to strong encapsulation stops at keeping class definitions in one statement (spiritual encapsulation rather than code encapsulation ).


Other libraries provide stricter support for public and private methods and attributes (encapsulation). Base. js provides a concise, easy-to-use, and easy-to-remember syntax.


Listing 4 provides an overview of Base. js and classic inheritance. This example uses a more specific RobotEnemy class to extend the features of the abstract Enemy class.


Listing 4. Introduction to Base. js and classic inheritance


// 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;
}
});


OOP mode in Game Design


The basic game engine inevitably relies on two functions: update and render. The render method usually performs requestAnimationFrame Based on setInterval or polyfill. For example, this is used by Paul Irish (see references ). The advantage of using requestAnimationFrame is that it is called only when needed. It runs according to the refresh frequency of the customer Monitor (usually 60 times a second for desktops). In addition, it is usually not run in most browsers, unless the game tab is active. Its advantages include:


  • Reduce the workload on the client when the user does not stare at the game
  • Saves power on mobile devices.
  • If the update loop is associated with the rendering loop, the game can be effectively paused.


For these reasons, compared with setInterval, requestAnimationFrame has always been considered a "Good Citizen" of "customer-friendly ".


Binding an update loop and a render loop brings about a new problem: You must keep the game action and animation at the same speed, regardless of whether the running speed of a loop is 15 frames per second or 60 frames. The trick here is to set up a time unit in the game, called a tick (tick), and pass the amount of time since the last update. Then, you can convert the time to the number of tick answers, while the model, physical engine, and other time-dependent game logic can be adjusted accordingly. For example, a poisoned player may suffer 10 damages for each tick answer, lasting 10 tick answers. If the rendering loop runs too fast, the player may not accept the damage in a certain update call. However, if garbage collection takes effect in the last rendering loop that causes the last half answer, your logic may cause 15 damages.


Another way is to separate the model update from the view loop. In games that contain a lot of animations or objects or draw games that occupy a lot of resources, the coupling between the update loop and the render loop will completely slow down the game. In this case, the update method can run at a set interval (using setInterval), regardless of when and how often the requestAnimationFrame handler will be triggered. The time spent in these loops is actually spent in the rendering step. Therefore, if only 25 frames are drawn to the screen, the game continues to run at the set speed. In both cases, you may want to calculate the time difference between update cycles. If you update a function 60 times a second, the function update can take up to 16 ms. If you run this operation for a longer time (or if your browser is running for garbage collection), the game will still slow down. Listing 5 shows an example.


Listing 5. Basic Application classes with render and update Loops


// RequestAnim shim layer by Paul Irish
Window. requestAnimFrame = (function (){
Return window. requestAnimationFrame |
Window. webkitRequestAnimationFrame |
Window. Required requestanimationframe |
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
});


If you are not familiar with the context of this in JavaScript, please note. bind (this) is used twice: one is an anonymous function in the setInterval call, and the other is this in the requestAnimFrame call. render. bind. SetInterval and requestAnimFrame are both functions rather than methods. They belong to the global window object and do not belong to a class or identity. Therefore, to make this Engine rendering and update method reference the instance of our Engine class, calling. bind (object) will force this function to behave differently from normal conditions. If you support Internet Explorer 8 or earlier, you need to add a polyfill for binding.



12/2 PagesNext Page

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.