“Heroes of Might and Magic” in the browser: long, difficult and unbearably interesting

    How to implement in a browser a game on which years ago it stuck without any browser? What difficulties will you encounter in the process, and how can they be solved? And finally, why do this at all?

    In December, at the HolyJS conference, Alexander Korotaev (Tinkoff.ru) described how he made a browser version of Heroes. Earlier a video report appeared, but now for Habr we also made a text version. To whom the video is more convenient - start the video, and to whom the text - read it under the cut:


    I would like to tell you about how I made in the browser those very third “Heroes” in which many of you, I think, played as a child.

    Before embarking on any interesting long journey, you should look at the route. I went to GitHub and saw a new clone of Heroes appear every two months. These are repositories with two or three commits, where literally several functions are added, and the person throws, because it is difficult to do. He understands the whole burden of responsibility that will fall on him if this is completed. Here I provided links to the most successful repositories that can be found:

    1. sfia-andreidaniel / heroes3
    2. mwardrop / HOMM3Clone
    3. potmdehex / homm3tools
    4. openhomm / openhomm
    5. vcmi / vcmi

    The last of them I highlighted to highlight its importance to the community, because it is the only fully written clone of "Heroes" in C, using a distribution of original resources that can be attached to it. And this is the only way to run the third “Heroes” on Android devices. They run through the emulator, the problem is that they are very slow, the touch interface is not available there, you have to move the mouse - in general, this is only for very big fans.

    What goals did I set for myself when I took up this?

    • I really wanted to do something, I wanted to jump above my head. Naturally, I wanted to show myself. In general, it was originally planned as a site of its own.
    • I also wanted to stop playing games in general, and in Heroes in particular. As you know, the best defense is an attack. You start developing games, you start playing them differently and much less.
    • And I also wanted to do something very beautifully, because I always strived for the beauty of the interfaces, and the toy itself is very beautiful.

    At first I tried to repeat the original picture:



    Below you can see the original editor and its simple render, and my simple render, which, however, cost no flags at that time. This is almost the first screenshot of the development of the game. By the way, perhaps it will also be useful for you to take screenshots of some of your project, which will kill someone, because one day it may be needed. I needed a screenshot for my report, although I didn’t initially plan it, I just wanted to keep the story. I thought the story was long and should be kept in pictures.

    And so, I practically repeated the picture of the original game, but I had to move on.

    To begin with, for those who are not aware of gamedev in JavaScript, I’ll tell you what a regular game consists of:

    • Data model. That is, this is some kind of map, characters, scene, simply where our objects are stored.
    • A game loop or game loop that calculates every second, doing some actions with objects and changing the model.
    • There is still user input processing. This is a reaction to input from the keyboard, joystick, mouse, anything.
    • And, the most beautiful part is the render that the model should render. In fact, the model changes, and the rendering works independently.

    If you imagine this in the form of code, everything is simple:

    01. const me = {name: 'Alex', left: 0}
    02. ...
    03. setInterval(() => update(), 1000)
    04. ...
    05. window.addEventListener('keyup', () => me.left++)
    06. ...
    07. requestAnimationFrame(() => draw())
    

    What lies behind this:

    • Line 01: model. She keeps something corny.
    • Line 03: game loop. This is setInterval, which calls the update () function.
    • Line 05: input processing. A regular EventListener for user events, which, for example, moves the character to the right.
    • Line 07: rendering. This is requestAnimationFrame, which allows us to call callback, aiming for 60 frames per second. When the browser is hidden, it is not called, otherwise it is drawn with the browser window, very convenient.

    You can read more about game dev on JS in the book "Surrealism in JavaScript" , open it at least for the sake of such wonderful pictures:





    A brief history of game development


    If you want to start making your “Heroes”, you have:

    1. Original game
    2. Map editor. The developers at first thought that he would allow the game to live a maximum of two years, how much they were mistaken!
    3. FizMig is a great reference for all game mechanics. Its remarkableness is that people empirically calculated all the probabilities of loss of skills, spells, any damage, and presented it in formulas and tables with a percentage ratio. People have been working for ten years, that is, they are very big fanatics, even I can not compare with them.
    4. There are many forums with guys who have delved into the “Heroes” for many years. By the way, the forums are Russian-speaking: the English-speaking guys almost did not dig.
    5. Unpacker of resources, thanks to which you can get pictures, data, anything.

    I started by rendering the usual green field, as in the first picture:



    Here you can see how I drew objects on the green field and debazhed their important points. Red dots are obstruction, yellow dots are some kind of action at this point. At the action castle, only where you can go, at the hero, on the whole model.

    Next, I worked with data. Data is a list of all skills, monsters, characters, cards, everything about text and binary files that needed to be read and somehow accumulated.



    Then I worked with algorithms. I didn’t get the algorithms right away. Here I tried to make a path search algorithm:



    But not everything worked smoothly, this is perhaps one of its best runs.

    I realized how much I was wrong when I tried to write it myself. However, nothing went smoothly for me, in fact I walked almost across the rake field. Fortunately, I did not give up and still tried to find a way out of this situation, and I somehow managed to do it.



    Parsing cards


    In the beginning there was a very important stage, it was relatively boring, complicated, and this is parsing cards. The fact is that if it were not for him, there would be nothing. Since it was not interesting for me to draw a field with objects that I superimposed on each other using some kind of offset, I wanted to read the original maps in order to have a convenient editor with which you can immediately see the changes in the game:



    When you open the map in In this editor, you see an excellent visual interface for editing any buildings, objects, and so on. It is convenient, clear and intuitive. Many thousands or tens of thousands of cards have already been made for the "Heroes", they are still in very large numbers.

    But if you want to read it as a developer, you will see that it is just binary code that is difficult to read:



    Я медитировал над этим кодом, находил какие-то бедные спецификации по тому, как он устроен и что у него есть внутри, и со временем я даже начал это читать. Буквально две недели на него смотрю, и уже начинаю видеть какие-то закономерности!

    Тут я понял, что что-то со мной не так, начал копаться и узнал, что нормальные ребята читают это в редакторах с поддержкой шаблонов:



    Для карт уже написаны шаблоны, которые позволяют парсить их в редакторе 010 Editor. В нем они открываются как в браузере. Вы видите что-то похожее на dev-tools, можете наводить курсор на какую-то секцию кода, и будет показываться, что там внутри находится. Это куда удобнее того, с чем я пытался работать раньше.

    Suppose there are scripts, it remains to write the code. In the beginning, I tried to do this in PHP because I did not know another language that could handle this, but over time I came across homm3tools . This is a set of libraries for working with different data of "Heroes". Basically, it is a parser of different card formats, a map generator, a render of inscriptions from trees, and even the game "Snake" from game objects. When I saw this craft, I realized that with homm3tools you can do anything, and the fanaticism of this person ignited me. I started talking with him, and he convinced me that I should learn C and write my converter, which I basically did:



    Actually, my converterallows you to take a regular map file for "Heroes" and turn it into readable JSON. Readable for both JavaScript and humans. That is, I can see what is in this map, what data is there and quickly understand how to work with it.

    There was more and more data, the number of objects grew, I ran all the big maps and saw that the resources were leaking somewhere. They became smaller and smaller, and even a small movement on this map caused friezes and brakes. It was very unplayable and ugly.



    Everything slows down!


    What should I do with this? I never came across this, and first went to look at drawing maps. A big card, probably, it slows down.

    But first, a little theory. Since everything is being drawn on Canvas, I would like to explain how it differs from the DOM. In the DOM, you just take an element, you can move it, and don’t think how it is drawn, you just move it and that’s it. To move and draw something on the Canvas, you need to erase it every time:

    01. const ctx = canvas.getContext('2d') 
    02. 
    03. ctx.drawImage(hero, 0, 0) 
    04. ctx.clearRect(0, 0, 100, 100) 
    05. ctx.drawImage(hero, 100, 0) 
    06. ctx.clearRect(0, 0, 100, 100) 
    07. ctx.drawImage(hero, 200, 0)
    

    If there is grass under the hero you are animating in this way, you have to draw grass:

    01. const ctx = canvas.getContext('2d') 
    02. 
    03. ctx.drawImage(hero, 0, 0) 
    04. ctx.drawImage(grass, 0, 0) 
    05. ctx.drawImage(hero, 100, 0) 
    06. ctx.drawImage(grass, 100, 0) 
    07. ctx.drawImage(hero, 200, 0)
    

    This is even more expensive and even more difficult, and in the case of very complex backgrounds it is generally a difficult task impossible.

    Therefore, I suggest drawing in layers:



    you just take layers, and the video card mixes them, which is what it should do. Thus, I greatly saved on redrawing, each layer is updated with its own order, drawn at different times. I got a more or less fast render, with which I could really do something complicated.

    I just use three Canvas which are superimposed on each other:


    Their names speak for themselves. Terrain - grass, roads and rivers.

    If you look at the terrain drawing algorithm, it might seem pretty resource-loaded:

    1. Take soil type tile
    2. Draw it with displacement and rotation, because the developers of the original game saved a lot of resources
    3. Overlay river
    4. Lay roads
    5. And there are still special soil types.

    And all this needs to be drawn, and preferably not in runtime. Therefore, I advise you to draw it immediately, as soon as you do the first render of the map, and put it in the cache. Drawing ready-made pictures is much cheaper than drawing roads anew every time you need them.

    How to move the map smoothly? I had problems with this, but I came across a solution from Yandex.Maps:



    The fact is that when you move the map, its transformation changes. This operation, as many know, is performed only on the video card, without calling Repaint. A fairly cheap operation to move a fairly large picture. But every 32 pixels I compensate for the left of this map back, in fact I just redraw it, but the user has the impression of a continuous movement of the map. What I wanted to achieve was implemented in Yandex.Maps, and so I realized.

    Then I started drawing objects, because I didn’t have enough optimization for one map. But for starters, a little theory. The fact is that the axis of drawing objects in the "Heroes" is inverted. In fact, objects are drawn from the lower right corner. Why is this done? The fact is that we look at the map from above, but in order to give the player the impression that he is looking at three quarters from the side, objects are drawn from the bottom up, overlapping each other.



    Algorithm for drawing objects:

    1. We sort the array by Y of the lower border of each object (textures of different heights, you need to take this into account)
    2. We filter those that do not fall into the window (to draw what a person does not see is expensive)
    3. A lot of different checks
    4. We draw an object texture
    5. If necessary, draw the player’s flag.
      And all this despite the fact that the number of objects can reach over 9000! What to do, how to draw it in runtime? I think it’s better not to draw this in runtime, and now I’ll tell you how.



      For starters, I found a drawing algorithm like renderTree. It is used, for example, in a browser to draw DOM elements that hang on top of each other with a Z-index. And each branch that is in this tree is the Y axis along which the objects are sorted. In turn, on each branch all objects are sorted along the X axis.

      What do we get from this? We get a cheaper iteration, because we can immediately cut off branches that do not hit the screen. And at each iteration on the branch, we will look at the X object, and as soon as we come across an object that doesn’t fit on the map, we stop iterating over this object. Thus, fewer objects are affected than if we were just going through an array. Also, we are immediately given the correct overlap of objects, because they are already sorted. Thus, competent data storage is obtained.

      Next, I went to the drawing function:

      01. const object = getObject(id)
      02. const {x, y} = getAnimationFrame(object)
      03. const offsetleft = getMapLeft()
      04. const offsetTop = getMapTop()
      05. 
      06. context.drawImage(object.texture, x - offsetleft, …
      

      We see that each function consists of the fact that I determine the final displacement of the object, its frame for animation, and, most importantly, the drawImage function. It all came down to this function, and somehow it was necessary to optimize it.

      I realized that I can simply create this function through bind with the necessary parameters and save it directly in renderTree. That is, I stopped storing objects there, and began to store only the drawing functions. Nothing more is needed there, so I got a great performance boost.



      But the matter is not only in objects, the fact is that the game should not slow down in terms of animation. Konyashka should run on the screen perfectly, otherwise you will have the impression that something is wrong with the game.

      Let's plunge a little into geometry to understand what we had to go through. There, when you draw a segment at a certain distance in any direction - at least horizontally, at least diagonally - they are equal.



      But this is geometry. And we have "heroometry". There the problem is that this is a game on a grid where the diagonal and horizontal displacement in fact are not equal, but the game believes that this is the same, and everything is fine.



      How to live with it? If we count, then for the horizontal movement we take four animation steps, for the diagonal - about six. I started looking for a solution on how to make this animation really smooth.

      The problem with JavaScript is that it is single-threaded and operates on tasks. Each setTimeout that we set creates a separate task, it competes with other tasks that we have, for example, with other setTimeout. And in this regard, nothing will save us.

      I tried to do it through setTimeout, through setInterval, through requestAnimationFrame - everything creates tasks that compete with each other.



      And with a large number of calculations when the player moves, competing tasks spoiled the whole animation for me.

      I went on to search further and found that in JavaScript, it turns out, there are microtasks that are part of the tasks. They are needed in those cases when the callback that you pass, for example, to Promise, the only object that does the microtask can be executed immediately, or asynchronously. Therefore, just in case, we implemented a microtask, which has a higher priority than the task.



      In fact, we get a non-blocking loop that can be used for animation. Read more about this in an article by Jake Archibald.

      To get started, I took everything and wrapped it in Promise:

      01. new Promise(resolve => {
      02. setTimeout(() => {
      03. // расчеты для анимации
      04. requestAnimationFrame(() => /* рисование */) 
      05. resolve()
      06. })
      07. })
      

      I still needed setTimeout to do the animation, but it was already in Promise. I did calculations for animation and fed into the requestAnimationFrame function what I needed to draw based on the results of these calculations, so that the calculations did not block the drawing, and it went when it really needed to.

      Thus, I was able to build a whole sequence of animation steps:

      01. startAnimation()
      02. .then(step)
      03. .then(step)
      04. .then(step)
      05. .then(step)
      06. .then(doAction) 
      07. .then(endAnimation)
      

      But I realized that this object is not very configurable and does not strongly reflect what I want. And I came up with storing animations in an object called AsyncSequence:

      01. AsyncSequence([
      02.    startAnimation, [
      03.        step
      04.        step
      05.        ...],
      06.    doAction,
      07.    endAnimation
      08. ])
      

      In fact, this is a kind of reduce, which is passed through Promise and calls them sequentially. But it is not as simple as it seems, the fact is that it also has nested animation loops. That is, after startAnimation, I could put an array from one step. Suppose there are seven or eight of them, how much is needed for the maximum diagonal animation of the hero.

      As soon as the hero reaches a certain point, reject appears in this animation, the animation stops, and AsyncSequence realizes that it is necessary to go to the parent branch, and doAction and endAnimation are already called there. It is very convenient to make complex animation declaratively, as it seemed to me.



      Data storage


      But not only thanks to the render, we can increase our productivity. It turned out that the data retardation is most of all slowed down, which was the biggest surprise for me in JavaScript.

      First, find the data that slows down the most, and this is the map. The whole map is a grid, it consists of tiles. Tiles are some kind of conditional squares in the grid that have their own texture, their own data set, and allow us to build a map from a limited number of textures, like all old games did.

      This data set contains:

      1. Type of tile (water, earth, wood)
      2. Tile patency / cost
      3. Event availability
      4. The “busy with” flag
      5. Other fields depending on the implementation of your engine

      In code, this can be represented as a grid:

      01.const map = [
      02. [{...}, {...}, {...}, {...}, {...}, {...}], 
      03. [{...}, {...}, {...}, {...}, {...}, {...}], 
      04. [{...}, {...}, {...}, {...}, {...}, {...}], 
      05. ...
      06. ]
      07.const tile = map[1][3]
      

      The same visual design as a tile grid. An array of arrays, in each array we have objects that contain something for the tile. We can get a specific tile by offset X and Y. This code works, and it seems to be normal.

      But. We have an algorithm for finding the path, which in itself is quite expensive, it has to take into account a lot of details that are not only in tiles, but also in objects. And when we move the mouse, the cursor changes depending on whether we can reach this point, whether the enemy or some action is at this point.



      For example, they pointed at a tree - a regular cursor appeared, because you cannot go to this point. And we need to show the number of days it takes to reach the point. That is, in fact, we drive the algorithm for finding the path constantly, and that same grid works very slowly.

      To get the tile property, I needed:

      1. Request an array of tiles
      2. Query array array for string
      3. Request tile object
      4. Request an object property

      Four heap calls, as it turned out, is very slow when we need to request a map many times for the path search algorithm.

      And what can be done about it? At first, I looked at the data:

      01. const tile = {
      02. // данные для отрисовки
      03. render: {...},
      04. // данные для поиска пути
      05. passability: {...},
      06. // данные которые нужны значительно реже
      07. otherStuff: {...},
      08. }
      

      I saw that each tile object consists of what is needed for rendering, what is needed for the path search algorithm, and other data that are needed much less frequently. They were called every time, despite the fact that they were not needed. It was necessary to discard this data and figure out how to store it.

      And I found that the fastest way to read this data is from an array.



      After all, the tile object can be divided into arrays. Of course, if you write this business code at work, you will have questions. But we are talking about performance, and here all the tools are good. We just take a separate array where we store the type of the object in the tile, or that the tile is empty, and with it an array of numbers for the path search algorithm, which is a simple unit / zero "the cell is passable or not."

      But for the path search algorithm, you need not only to find out if there is an object or not, and put a unity or zero. Different types of soil have different passability, different heroes walk in different ways, all this must be considered.



      This simple array is considered by complex algorithms from two large arrays: with tiles and with objects. Thus, we get already calculated numbers that can be quickly used in the path search algorithm. We count in advance when the object is updated, and the values ​​are updated.

      As a result, we have many arrays that cache something and bind something:

      • Array of render functions for the render loop
      • Array of numbers to find the path
      • Array of strings to associate objects with tiles
      • Array of numbers for additional tile properties
      • Map objects with their ID for game logic

      All that remains is a timely update of data from slower storages to faster ones.

      Of course, the question has ripened how to get away from an array of arrays, which works much slower than a regular array.

      In fact, I switched to a regular array, just expanding the array of arrays, it works 50% faster:



      Getting the data offset in the array is simple. We just need to know Y, the width of this square, which we store in this array, and X.

      Further, more. I looked and understood that at each iteration I need to calculate the X and Y of the object from the index in the array. Each iteration had to do something, and, depending on X and Y, make some kind of decision:

      01. const map = [{...}, {...}, {...}, {...}, ...] 
      02. 
      03. const tile = map[y * width + x] 
      04. map.forEach((value, index) => {
      05. const y = Math.floor(index / width)
      06. const x = index - (y * width)
      07. })
      

      There are expensive operations here, such as multiplication, rounding, and division. Perhaps the most expensive division here was, and I began to look for what to do about it.

      Then I got acquainted with the power of two:



      I did not call this slide “Power of 2” for nothing, because it translates simultaneously as “power of two” and “degree of two”, that is, the power of two in its degree. And if you learn to work with bit shifts, which I highlighted in yellow, then you can increase productivity.

      The problem is that if this occurs in your business code, you will most likely be scolded too, because it is an incomprehensible code. And you can only find use for this power of two if you can understand how to work with these formulas.

      Although a left shift, which is equal to multiplication, will not give you a significant increase in productivity, but a right shift, which is comparable to division, gives a fairly large increase, and in combination with the fact that we divide only by two, our numbers are predictable, without fractions performance increases even more.

      Thus, I have come to the point that we can calculate the nearest power of two greater than we need to make a larger array in advance, but square, with a power of two that covers all of our storage needs.

      01. const map = [{...}, {...}, {...}, {...}, ...] 
      02. const powerOfTwo = Math.ceil(Math.log2(width)) 
      03. 
      04. const tile = map[y << powerOfTwo + x] 
      05. map.forEach((value, index) => {
      06. const y = index >> powerOfTwo
      07. const x = index - (y << powerOfTwo)
      08. })
      

      Suppose a card is 50x50, we find the nearest power of two greater than 50 and use it for further calculations (when getting X and Y, as well as shifting in an array to get a tile).

      Oddly enough, the same optimizations are present in the video card: The



      video card decomposes each texture, for which the so-called MIP mapping is provided, into squares-two degrees, which are drawn depending on the distance of the object. This gives us very cheap smoothing and very fast rendering, because everything that is a power of two is considered processor very quickly.

      So I got a Grid. Grid is a type of data storage, which is very convenient for me, which allows iterating, receiving immediately X and Y of each object, and, conversely, getting the object by X and Y.

      01. const grid = new Grid(32)
      02. 
      03. const tile = grid.get(x, y) 
      04. grid.forEach((value, x, y) => {})
      

      The problem is that only square grids can be stored this way, but I also store rectangular grids there, simply because it is fast. And the minus of the grid is that it is ineffective for meshes with a side greater than 256: if you multiply this, it becomes clear how much is in the data array. And such arrays slow down always and everywhere, and nothing has been invented for them. But I don’t need it, because there are no cards larger than 256x256, everywhere everything is pretty beautiful.



      UI on Canvas


      Then I started developing UI on Canvas. I looked at different toys, and mainly in toys UI was made in HTML. It was superimposed on top, so it was easier to develop, easier to adapt. But I wanted to rest in full and do the drawing.

      At first I began to create ordinary objects, passing some data into them, hanging eventListener on them. And it worked while I had two or three buttons.

      01. const okButton = new Buttton(0, 10, 'Ok') 
      02. okButton.addEventListener('click', () => { ... }) 
      03. const cancellButton = new Buttton(0, 10, 'Cancel')
      04.cancellButton.addEventListener('click', () => { ... })
      

      Then I realized that the amount of data is growing and growing, and I began to transfer objects there. There is also a “bindil” of the event, because it was convenient.

      01. const okButton = new Buttton({
      02. left: 0,
      03. top: 10,
      04. onClick: () => { ... }
      05. })
      06. const cancellButton = new Buttton({...})
      

      Then the number of objects grew, and I remembered that there is JSON.

      01. [
      02.    {
      03.        id: 'okButton',
      04.        options: {
      05.            left: 0,
      06.            top: 10,
      07.            onClick: () => { ... }
      08.        },
      09.    },
      

      Then I started to feel sad because I could not imagine how he would look. When you write code, you do a little bit of it in your head. When you typeset, you render a little. And I, trying to make up, tried to visualize, and it was very difficult.

      Then I remembered that there is XML. XML is the same as HTML, it’s clear and simple for me, and when building it, it generates the same JSON that the machine understands, but it is poorly understood to me.

      01. 

      In fact, I made a convenience for myself and a more expressive layout. I even came up with a computed condition that fires on the right event.

      Thus, my interfaces became much more complicated, and I began to operate with groups of elements that moved relative to each other, and began to make complex components. Abstractions only improved my code, they allowed me to think on a completely different level of complexity.

      01. 
      02.    
      03.       
      04.       

      As it turned out, I was not the first who came up with this - to do something from XML on Canvas. There is such a library - react-canvas , and I was very happy when I found out that my thoughts were also familiar to someone, and I thought of something useful that could be useful in other industries.



      How does it all work


      We examined separately the render, performance, reading data, storing it ... Perhaps you have a question: how does it all work together? But something like this:



      I drew a diagram that shows that I have a zone for quick access to data, which, receiving some event from the user, can quickly change something in the render, but there is a zone of long access - This is a model for storing a large number of objects. That is, everything is stored in long access, somewhere in the models, and asynchronously comes into the render.

      Dotted arrows represent asynchronous interaction. Resources are what we download from the server. When we need to load a picture, it will naturally come to us asynchronously. We do not download all the pictures, because, moving on the screen, we try to load only the necessary. I hope you do the same on sites.

      I would like to consider how this all works, using the example of resource collection. We see some kind of resource, run to it and collect it. How does the game work in this case?

      First, the path search is turned on:



      I am using the A * algorithm. This algorithm allows you to search paths in graphs. A graph is what can be represented in the form of a grid, either square or hexagonal, as in combat. In fact, on the battle screen and map screen, the same path search algorithm is used - the algorithm is reusable, and this is a big plus. It takes into account the "weight" of movement in each cell (in the picture on the left you can see that the hero will not go straight, but along the road, because it is corny cheaper, spend less steps).

      Further, during the movement of the character, his animation is performed. During the animation, I need to update the hero in the render tree. Why is this needed? The fact is that, since objects are drawn one above the other, when the hero is behind the mill, it overlaps it, and vice versa:



      To achieve such an effect at the right time, I need to do the transfer of the hero along the branches of the draw tree every time he passes the tile.

      Then I need to make checks at the end point, that is, when the hero has literally one step left, the checks begin:

      • We make a request to the map and get the ID of the objects at this point
      • They are sorted as: actions passable and impassable
      • We take the first object by ID
      • Check if you can go to the object to activate the action.

      In order to perform an action with an object, I have implemented PubSub in the events object in each of them:

      01. const objectInAction = Objects.get(ID) 
      02.const hero = Player.activeHero 
      03. objectInAction.events.dispatch('action', hero) 
      04. ...
      05. this.events.on('action', hero => {
      06. hero.owner.resources.set('gems', this.value) 
      07. this.remove()
      08. })
      

      So I can dispatch events and already inside the object, starting from the fifth line, I can hang up a callback on this action (in this case, I throw the action “action” and the only attribute that called it is the hero. In the object I get this hero, I list him the necessary resources and self-delete).

      By the way, removing an object is not so simple, perhaps this is the most difficult operation, because I need to update a lot of related arrays:

      • Remove rendering from the render
      • Delete path search arrays
      • Delete from the associative array with coordinates
      • Delete event handlers
      • Delete from the array of objects
      • Updating the minimap, already without this object
      • We are sending an event about the removal of this object from the current state (in order to save / load, I store the data in the state, this is a separate interesting challenge)

      Over time, I wondered how to update all these arrays faster. It turned out that the proportion of dynamic objects that can move or move is only about 10%, and this is perhaps the maximum. Thus, we have a ballast of 90% of the objects that we iterate each time when we need to update something in these arrays. And I saved a lot of money on the calculations, making two grids, which then merge when I really need it.

      I have a basic grid with static objects and a grid with dynamic objects, because most often I have to update and check only dynamic objects. If I don’t find the object in the dynamic grid, I climb into the larger and more expensive static grid, which contains more, and there exactly what I need will already be found. In this way, I increase performance when reading data. I advise you to always look at the data, are they really needed right now? Is it possible to separate them so that they can be read faster, and to store some long, large data separately and only read if necessary?

      How are objects arranged? Since this is a game, OOP fits perfectly on it:

      01. // Объект содержит гарнизон и может быть атакован
      02. @Mixin(Attacable)
      03. class TownObject extends OwnershipObject {...}
      04. // Содержит все для отрисовки флажка, его смены и т.п. 
      05. class OwnershipObject extends MapObject {...}
      06. // Содержит все базовые поля для объекта карты 07.class MapObject {...}
      

      Some objects are extended by others, thus receiving some properties from their parents. I also really love mixins that allow me to add some kind of behavior. For example, TownObject, which is the object of the city, is also Attacable, because it can be attacked. This means that he has his own garrison, there are functions for working with this garrison, there are callback functions that say what to do if the city was attacked (if there is a garrison, then join the battle, if not, then just give up).

      TownObject itself is inherited from OwnershipObject, which contains everything you need for objects that can be captured and checked. It has all the functions for setting a flag, for rendering it, for events that are needed when an object is captured by some other hero. And all this, in turn, is inherited from the base MapObject, where all the data for the base objects that we have is stored.



      conclusions


      What conclusions can I draw from all this? It was a very big fight against evil spirits. The evil was that there were many bugs, I was discouraged many times, I threw the project (it happened for months). This, by the way, is useful to do as part of your home project. Of course, as part of a working project, you are unlikely to be able to do this, but a home project allows you to be a little lazy and relax in order to come up with something more beautiful than you have now.

      Many people ask: why did you do this? I have been doing this for two years. The question is, why are you doing something big and not showing anyone? I’m showing it on the big screen, perhaps the second time, and there were different tips, like: “why don’t you make a plug-in for webpack or some small library and grab stars, and everything is in chocolate”. But I continued to do this, I continued not to show anything to anyone, except for a few friends who sometimes threw links. Thanks to my wife, who endured this for a long time!

      What it gave me:

      • I am very self-developed
      • I went beyond the usual work tasks. The fact is that when I started making this game, I worked in a regular web-studio, made websites, the scope of work tasks was strictly limited by what a site needed, and these are usually repeated tasks.
      • I greatly expanded my horizons, doing game tasks, doing game logic.
      • I also learned many fanatics who also do something for Heroes. Many of them did this far from two years, but five to ten years. Someone makes their own converter, someone in five years makes a cool card. That is, there are a lot of fanatics, they are not very PR themselves, they inspired me to move on and not stop. Acquaintance with fanatics is very inspiring.

      Why make games:

      • In my opinion, this is much more interesting than making sites, because you solve problems that you usually don’t
      • Это большое количество новых для вас алгоритмов, с которыми вы не сталкиваетесь. Например, я изобретал новые способы хранения данных, или, допустим, написал алгоритм поиска пути, который банально был в составе, но для того, чтобы сделать его быстрее, мне пришлось в нем разобраться и немножко дописать.
      • Это очень красиво. Советую вам наполнять мир красотой, потому что я всегда к ней стремился, и мне нравилось делать интерфейсы.

      In my opinion, the degree of mastery is directly proportional to the time that can be spent on work inland. When I learned to draw, we were told that when you draw your head, you must act as a sculptor. The sculptor first takes the parallelepiped of the stone and cuts off the edges from it, making it remotely look like a head. Then he begins to look for more and more new faces, he finds the shape of a nose, finds the shape of an eyebrow and in the end he finds 50, or even more, faces in a century.

      And the work inland is how long you can deepen your brainchild, how long you can work on it. And if you see your brainchild, and there are no options what else can be done, then something is wrong with the degree of mastery. I advise you to read and expand your horizons, relax and return again. Thus, you will only improve and make yourself a great master.

      Here I left useful links that partly helped me:


      And, of course, a demo , where would it be without her. It also works on phones.
      Minute of advertising. If you liked this report from the previous HolyJS, pay attention: HolyJS 2018 Piter will be held on May 19-20 . And its program has already been published on the conference website , so see what of its reports will be interesting to you!

    Also popular now: