preface

The first version of the game was developed in 2014. The browser side used HTML + CSS + JS, the server side used ASP + PHP, the communication used Ajax, and the data storage used Access +mySql. However, due to some problems (not using Node at that time, writing complex logic in ASP can really write vomit; At that time, canvas was also written less, dom rendering is easy to reach performance bottlenecks), has been abandoned. Later, it was remade with Canvas. This article was written in ’18.

Data summary

  • Project address and Demo please pay attention to: github.com/chenzhuo199…

  • The project uses the data-driven lightweight Canvas rendering library Easycanvas: github.com/chenzhuo199…

  • Easycanvas Chrome debug plugin: github.com/HuJiaoHJ/ec…

  • Mir ii client file analysis: github.com/jootnet/mir…)

  • This article continues to update in nuggets, reprint please note: juejin.cn/post/684490…

1. Preparation before development

Why use Javascript to implement a complex PC game

1. It is feasible to implement PC-end online games with JS. With the upgrade of HARDWARE configuration of PC and mobile phone and the updating of browser, as well as the development of VARIOUS H5 libraries, js is becoming less and less difficult to achieve an online game. The difficulty here is mainly in two aspects: browser performance; Is the JS code extensible enough to accommodate iterations of a logically complex game?

2. There are few large-scale JS games for reference at this stage. Most (and almost all) games involving multiplayer, server-side data storage, and complex interactions are developed in Flash. But Flash is on the wane, while JS is growing fast and can be run with a browser.

Why did you choose a 2001 mir ii game

The first reason is nostalgia for old games; The other reason, of course, is that other games either I don’t know how to play or I know how to play but don’t have the material (graphics, sound effects, etc.). I think it’s a waste of time to spend a lot of time collecting maps, character and monster models, items and equipment diagrams for a game, and then processing and parsing them for JS development.

Since I collected some legendary game materials before, and fortunately found a way to extract the resources file of the Legend of Mir II client, I could directly start to write code, saving some preparation time.

Possible difficulties

1. Browser performance: This is probably the most difficult one. If the game is going to hold 40 frames, then only 25ms per frame is left for JS to calculate. And because rendering is usually more performance intensive than computation, it actually only leaves JS around 10 milliseconds.

2. Anti-cheating: How to avoid users directly calling the interface or tampering with network request data? Since the goal is to use JS to achieve more complex games, and any online game needs to consider this, there must be a relatively mature solution. That’s not the point of this article.

2. Overall design

The browser

  1. Canvas is used for rendering.

    Compared with DOM (div)+ CSS, Canvas can handle more complex scene rendering and event management. For example, the scene below involves four images: players, animals, objects on the ground, and the lowest level map image. (There are actually shadows on the ground, names for people, animals, objects, and shadows on the ground. For the sake of easy comprehension, let’s not consider so much.)

    At this time, if you want to achieve “click animals, attack animals; Click on the item, pick up the item “effect, then need to listen for the animal and item events. When using the DOM approach, several difficult issues arise:

    • The order of rendering is different from the order of event processing (sometimes small z-index events need to be processed first), requiring additional processing. For example, in the example above: it is easy to point characters when clicking monsters and items, so it is necessary to do “click event penetration” processing for characters. And the order of events is not fixed: if I have a skill (such as healing in the game) that requires a character to cast, then the character needs an event listener. So whether or not an element needs to handle events, and in what order, varies with the state of the game, and dom event binding is no longer sufficient.

    • Related elements are difficult to place in the same DOM node: for example, the player’s model, the player’s name, and the skill drawing on the player, ideally in a

      or

      container for easy management (so that the positioning of several elements can inherit the parent element without having to deal with the location separately). But then, z-index is going to be really hard to deal with. For example, if player A is above player B, player A will be blocked by player B, so the Z-index of Player A needs to be smaller, but the name of player A needs not to be blocked by player B’s name or shadow, which cannot be achieved. To put it simply, the maintainability of the DOM structure sacrifices the visuals and vice versa.

    • Performance issues. Even at the cost of effects, dom rendering is bound to have a lot of nesting, with all elements changing styles frequently, triggering repaint and even reflow in the browser.

  2. Canvas rendering logic is separated from project logic

    If the various rendering operations of canvas (such as drawImage and fillText, etc.) are put together with the project code, it will inevitably lead to the failure of maintenance in the later stage of the project. After reviewing several existing Canvas libraries and combining vue’s data binding + debugging tools, we developed a new Canvas library, Easycanvas (Github address), which supports debugging elements in Canvas through a plug-in just like Vue.

    This way, the entire rendering part of the game is much easier, just need to manage the current state of the game, and update the data according to the server sent back from the socket. Easycanvas is responsible for the link “data changes cause view changes”. For example, as shown in the figure below, we only need to give the location of the package container and the layout rules of each element in the backpack, and then bind the items in each package to an array, and then manage the array (the process of data mapping to the screen is handled by Easycanvas).

    For example, the style of 40 items in 5 rows and 8 columns can be passed to Easycanvas in the following form (index is the index of items, the spacing of items in x direction is 36, the spacing of items in Y direction is 32). This logic is immutable; no matter how the array of items changes or where the package is dragged, the relative position of each item is fixed. As for canvas rendering, it does not need to be considered by the project itself, so it has good maintainability.

    style: {
        tw: 30, th: 30,
        tx: function () {
            return 40 + index % 8 * 36;
        },
        ty: function () {
            return31 + Math.floor(index / 8) * 32; }}Copy the code
  3. Canvas layered rendering

    Assume: the game needs to hold 40 frames, have a browser 800 wide by 600 high, and have an area of 480,000 square feet.

    If you use the same canvas for rendering, then the frame count of this canvas is 40, and you need to draw at least 40 screen areas per second. However, the same coordinate point is likely to overlap multiple elements, such as the UI at the bottom, the health bar and the button, which together block the scene map. So together, the browser can easily draw more than 100 screens per second.

    This drawing is difficult to optimize, because the view is being updated everywhere on the canvas: it could be player and animal movements, it could be button effects, it could be a change in the effect of a skill. In this case, even if the player does not move, the entire canvas will be redrawn due to the “wind” effect of the clothes (which is actually the Sprite animation playing to the next image) or a potion appearing on the ground. Because it is almost impossible for a frame to be indistinguishable from the last, it is hard to keep even a part of the game frame unchanged. The graphics throughout the game are constantly updated.

    Because it’s almost impossible for one frame to be the same as the last, it’s always updated.

    Therefore, I adopted the overlapping arrangement of 3 canvas this time. Since Easycanvas event processing supports passing, even if the top canvas is clicked, if no element ends a click, the following canvas can also receive the event. Three Canvas are responsible for UI, ground (map) and sprites (characters, animals, skills and special effects, etc.) respectively:

    The advantage of this layering is that the maximum number of frames per layer can be adjusted as needed:

    • For example, the UI layer, because many UIs are usually not moving, even if moving will not need too precise drawing, so you can appropriately reduce the frame number, for example, to 20. This way, if the player’s health drops from 100 to 20, the view can be updated in 50ms, and the 50ms switch is invisible to the player. Since changes in UI layer data such as stamina are difficult to change multiple times in a short period of time, and 50ms delays are hard to perceive, frequent drawing is not required. If we save 20 frames per second, we can probably save 10 screen areas of painting.

    • On the ground, the map changes only when the player moves. This saves 1 screen area per frame if the player does not move. The maximum number of frames on the ground should not be too low due to the need for smooth movement. If the ground is 30 frames, you can save 30 screen areas per second when the player is not moving (in this case, the map is almost full screen). The movement of other players and animals does not change the ground, nor does it require redrawing the ground layer.

    • The maximum number of frames for the Sprite layer cannot be reduced. This layer shows the core parts of the game, such as character actions, so the maximum number of frames is set to 40.

    Thus, the area drawn per second may be 80 to 100 screen areas when the player moves, but only 50 screen areas when the player does not move. In the game, the player stops to fight monsters, type, organize objects, and release skills while standing still, so the ground is not drawn for a large amount of time, resulting in significant performance savings.

The server side

  1. Since the goal is to achieve a multiplayer online game with JS, the server uses Node and uses socket to communicate with the browser. As an added benefit, some common logic can be reused at both ends, such as determining whether an obstacle exists at a coordinate point on a map.

  2. Node terminal player, scene and other game-related data are stored in memory, periodically synchronized to files. Each time the Node service starts, data is read from the file into memory. As a result, the frequency of file reads and writes increases exponentially with more players, causing performance problems. (Later, in order to improve stability, a buffer was added for file read and write, “memory-file-backup” mode, so as to avoid file damage caused by server restart in the process of read and write).

  3. Node consists of interfaces, data, and instances. The interface is responsible for interacting with the browser. “Data” is static data, such as the name and effect of a drug, the speed and stamina of a monster, that is part of the rules of the game. An “instance” is the current state of the game, such as a drug on a player, an instance of “drug data.” For another example, “Instance of deer” has the “current health” attribute. Deer A may be 10, deer B may be 14, and deer itself only has “initial health”.

3. Realization of scene map

Map scene

Now we will introduce the map scene, which still relies on Easycanvas for rendering.

thinking

Since the player is always anchored to the center of the screen, the player’s movement is, in effect, the map’s movement. For example, if the player runs left, the map pans right. As mentioned earlier, the player is in the middle of the three canvas, and the map is at the bottom, so the player must block the map.

This seems reasonable, but if there is a tree in the map, then “the player is always higher than the tree” is not true. At this point, there are two big solutions:

  • The map is layered, separating “ground” from “ground”. Place the player between two layers, such as the one below, with the ground on the left and the ground on the right, and draw them on top of each other, placing the character in the middle:

    This seemingly solves the problem, but in fact introduces two new problems: the first is that the player can sometimes be blocked by something “on the ground” (like a tree), and sometimes needs to be able to block something “on the ground” (for example, if you stand under this tree, your head will block the tree). Another problem is that the performance cost of rendering increases. Since the player is constantly changing, the “ground” layer needs to be redrawn frequently. This also breaks the original design, which minimizes rendering of large ground maps, resulting in more complex canvas layering.

  • The map is not layered; the “ground” is drawn together with the “ground”. When the player is behind the tree, set the player’s transparency to 0.5, as shown below:

    There’s only one downside to this: the player’s body is either opaque or translucent (monsters walking on the map can also do this) and not completely realistic. The ideal effect is to have part of the player’s body covered. But it’s performance-friendly and the code is easy to maintain, and I’m using it now.

So how do you tell which parts of the map are trees? Games often have a large map description file (essentially an Array), with numbers like 0, 1, 2 to indicate which places are passable, which places are obstructions, which places are teleports, and so on. The “description file” in the Legend of Mir II is described in 48×32 units, so players’ actions in the legend will have a “checkerboard” feel. The smaller the unit, the smoother it is, but the larger the footprint, the more time it takes to generate this description.

Let’s get started.

implementation

I asked a friend to help me export the map of “Bizhi Province” in the legend of Mir II app. It is 33,600 wide and 22,400 high, which is hundreds of times the size of my computer. In order to prevent the computer from exploding, it needs to be broken up into multiple pieces and loaded. Since the smallest unit of legend is 48×32, we split the map into 4900 (70×70) image files at 480×320.

We set the canvas size as 800×600, so that the player only needs to load 3×3 and a total of 9 pictures to cover the entire canvas. 800/480=1.67, so why not 2×2? It is possible that the player’s current position will cause some images to show only part of the image. I drew a beautiful schematic diagram:

So, at least 9 images in a 3×3 arrangement are needed to “fill” the canvas. However, there is a danger in doing this, that is, the size of each 480×320 map fragment file should be at least tens of KB. If it is drawn when necessary, it will cause the characters to see the blocks loaded one by one when running, which will affect the experience. So I used 4×4 to fill the canvas with 16 blocks. In this way, some redundant area is reserved for the effect of map translation, and the loading time of picture files is advanced, which plays a preloading effect. There is no need to consider whether rendering performance is wasted, because the canvas size is 800×600, and when we draw outward-for example, a block with an abscissa of 900 ~ 1380, we don’t really “draw” and there is no performance waste. (To repeat, when drawing outside of canvas using the drawImage method native to canvas, my test results show that the performance cost is very low. In Easycanvas library, I have encapsulated the native method of Canvas: when judging that the drawing area exceeds canvas, the drawing will be clipped; The drawImage method is no longer executed when the drawing area exceeds the canvas.

We added a map container to the canvas (to hold the 16 blocks) via Easycanvas. The top left vertex of the container is located at the top left of the browser point (0,0) to ensure that the container completely covers the canvas. One thing to note is that the map container will only move slightly within a block, with the maximum horizontal and vertical distances of 480 and 320. Taking the horizontal direction as an example, if the first four blocks in the container are T15, T16, T17, and T18, then as the player runs to the right, the four blocks start to shift to the left. When the player has run 480 distances (in fact, the container has run 480 distances), the container can be immediately put back (move 480 back to the origin), and the four blocks become T16, T17, T18, and T19. Thus, the style of the container is to mod 480 and 320, and then add the appropriate corrections:

var $bgBox = PaintBG.add({
    name: 'backgroundBox',
    style: {
        tx: function () {
            return- ((global.pX - 240) % 480) - 320; }, ty:function () {
            return- ((global.pY - 160) % 320) - 175; }, tw: 480 * 4, // As a container, th: 320 * 4,'lt', // tx, ty as the top left vertices to Easycanvas}});Copy the code

Then add 16 blocks to the container. The code for adding blocks is relatively simple. Here is the number algorithm for each block (assuming that the file name of the image corresponding to each block is 15×16.jpg) :

content: {
    img: function () {
        var layer1 = Math.floor((global.pX - 240) / 480) + i - 1;
        var layer2 = Math.floor((global.pY - 160) / 320) + j - 1;
        var block = `${layer1}x${layer2}`;
        return Easycanvas.imgLoader(`${block}.jpg`); }}Copy the code

Where, I and J represent the serial numbers (0-4) of blocks. The calculation method of layer is not unique, it can be adjusted according to the algorithm of the container.

In this way, the map pans when the player’s coordinates pX and pY change. The player runs to the right and the map moves to the left (so tx above needs a negative sign, because tx here is analogous to computed in vUE syntax). The location of the map container is determined by the player’s coordinates, and it is redrawn only with the player’s coordinates, without any interference from other data. In this way, on the one hand, the data and the view are bound, on the other hand, the data flow is one-way, will not be interfered by other modules, and does not need to be interfered by other modules.

4. Implementation of UI layer

Start with the UI layer (last due to the complexity of the Sprite layer).

Implementation of the bottom UI

The UI at the bottom of mir ii is a larger picture:

This picture is called “bottom UI”. The bottom UI is 800×251, which is about half the game screen. So at the beginning of the design, it was mentioned to separate the UI and put it on a separate canvas, and then draw it in low frequency. So button, chat box, blood cells should be cut out separately?

For example, should the 4 small blue buttons on the right be separated from the bottom UI to write the rendering logic separately?

Button in the bottom UI

The key to determining whether a part needs to be removed from the whole is to see if the whole and the part are not rendered at the same time. For example, at some point the bottom UI exists and the button is missing, then the button must be cut out. You may ask: this part needs to be changed, for example, when the mouse presses the button, the button will glow, so should it be cut out? The answer is no. It is perfectly possible to place a “glow button” where the button is, and make its opacity zero, and change to 1 when the mouse is pressed:

UI.add({
    name: 'buttomUI_button1',
    content: {
        img: './button1_hover.png'Opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0, opacity: 0mousedown () {
            this.style.opacity = 1;
        },
        mouseup () {
            this.style.opacity = 0;
        },
        click() {/ /... }}});Copy the code

Also, since the buttons are normal most of the time, this is the most performance-friendly way to do it. At the same time, this design also allows the bottom UI to only render, and a child of the bottom UI corresponds to each click event, which is also easy to maintain the code.

Blood of the spherical

The spherical blood bar in the Legend of MIR ii looks like a three-dimensional thing, but it’s just a picture switch. Assume that the image of the empty ball is empty. PNG and that of the full ball is full.png.

For example, if the player has a maximum mana of 100 and currently has 30 left, it can be interpreted as drawing full.png on the bottom 30% and empty.png on the top 70%. However, for the sake of simplicity and performance, you can put empty.png on the bottom of the UI (see the bottom UI image above) and then cover it with full.png based on the current health. In this way, there is no layer corresponding to the “empty state”, but it is used as the background, and the “full state” of various lengths is overwritten on top according to the current state.

The image below shows how the “health bar” can be achieved by overlaying a full tile:

As you can see, if the blood volume is full, we can completely cover the full graph; When the amount of blood is not enough, we can cut part of the picture from the full state to cover the empty ball. We bind their clipping range (sx, SY, SW, sh parameters in Easycanvas, where S stands for source and refers to the source image) with the data layer and pass it to Easycanvas (the size of the full-state hemisphere is 46×90). There are many variables involved in calculation, which are described one by one below.

var $redBall = UI.add({
    content: {
        img: 'full_red.png'
    },
    style: {
        sx: 0,
        sw: 46,
        sy: function () {
            return (1 - hpRatio) * 90;
        },
        sh: function () {
            return hpRatio * 90;
        },
        tx: CONSTANTS.ballStartX,
        ty: function () {
            return CONSTANTS.ballStartY + (1 - hpRatio) * 90;
        },
        tw: 46,
        th: function () {
            return90 * hpRatio; }, opacity: Easycanvas. Transition. Pendulum (0.8, 1, 1000). The loop (), locate:'lt',}});Copy the code

Since the position of the ball from the left side is fixed no matter how the blood volume changes, tx and Sx are constant values. Tx is a constant measured from the bottom UI, and sx is 0 in order to draw from the far left of the source image.

We set the ratio of current blood volume to maximum blood volume as hpRatio, so when hpRatio is 1, the blood volume is full. At this point, we do not need to crop the source image, we draw the full height of the blood cells. So the height of the plot is proportional to hpRatio.

When the health is low, we should start from the middle of the source image and draw the middle to the bottom. Therefore, the smaller the hpRatio, the larger the cutting starting point sy. And the starting point sy of y direction clipping is related to the height sh of clipping: sy+sh=90. Similarly, the lower the hpRatio, the lower the blood volume, and the lower the starting point.

For opacity, let it loop slowly from 0.8 to 1. This gives the player the feeling that the blood is “flowing”. (If we had multiple images, it would be more realistic to rotate them.)

At this point, the development of spherical blood strip is completed. The view is completely data-driven, and every time the blood volume changes, we calculate a new hpRatio, and the blood cells update accordingly. It is still a one-way data flow from data to view, which ensures that the view presentation is only numeric driven for subsequent extension. For example, “player takes medicine to replenish health” doesn’t need to be concerned about how the ball health bar should change, it just needs to be associated with the data.

Knapsack (player’s belongings)

Backpacks involve extremely complex interactions, the main points being:

  • View binding to an Array of items. The view needs to be updated when the item data is updated. This is the most basic function.

  • Each item has very complex events. Double click items to use. After clicking the item, the item moves with the mouse.

    If you click on the ground, you need to drop the item to the ground (actually sending the server to drop the item). [Fixed] If you click on a slot in the character equipment bar, you can wear or replace equipment. If you click on a slot in the warehouse, the event becomes a stored item; If you click on the backpack, it could be to put the item back, or it could be to swap the location of the two items… There are many, many more.

  • Backpacks can be dragged anywhere and can coexist with other backpack-like “dialog UI”. Then there must be a hierarchical computing relationship between several dialog boxes like backpacks. I drag the backpack dialog onto the Characters dialog, so the backpack’s Z-index is larger. If you click on the characters dialog box, the Z-index of the characters dialog box will be higher. What if another NPC dialog pops up?

  • In the legend of Mir II game, WHEN I drag my backpack to any place, when I open the warehouse, the system will automatically arrange: the warehouse appears on the left, and the backpack immediately moves to the right, convenient for the player to operate. Algorithms are involved to make these dialogs feel “smart” to the player.

Warning, high energy Warning ahead.

The player might also do something like this:

  • Open the backpack, then left-click on the ground, the character starts running. The player’s mouse moves to and fro to control the characters running on the map. Then the mouse moved to the backpack, stay on a certain item, then raise the left button, (@* (#)…… (@ # @ #!

  • Let’s say the number 1 corresponds to a skill, and the player suddenly clicks on an innocent potion in the backpack while dragging it (even if the player is stupid, at least make sure our JS doesn’t give an error).

  • A case that could not be clearly described in hundreds of words is omitted here.

Start writing code. First of all, there must be a backpack container:

var $dialogPack = UI.add({
    name: 'pack',
    content: {
        img: pack,
    },
    style: {
        tw: pack.width, th: pack.height,
        locate: 'lt',
        zIndex: 1,
    },
    drag: {
        dragable: true,
    },
    events: {
        eIndex: function () {
            return this.style.zIndex;
        },
        mousedown: function () {
            $this.style.zIndex = ++dialogs.currentMaxZIndex;
            return true; }}});Copy the code

Style is not much to say, zIndex let’s write a random 1 up. Drag is a drag API provided by Easycanvas, there is not much to say. Event eIndex (Easycanvas is used to manage the index of event trigger order, event-zIndex) needs to be synchronized with zIndex, after all, which dialog box the player sees above, which dialog box must capture the event first.

However, we need to bind the Mousedown with an event: when the player clicks on the dialog box, its zIndex is raised to the highest of all the current dialogs. We let all dialogs get “current maximum zIndex” from a common dialogs module. After each setting, the maximum zIndex increases by 1 for the next dialog.

So the container is going to do that, and now we’re going to fill it in. Let’s make the backpack Array global.pack and use a for loop to fill 40 squares with items with index I:

$dialogPack.add({
    name: 'pack_' + i,
    content: {
        img: function () {
            if(! global.pack[i]) {return; // If there is no item in the ith grid, it will not render.return Easycanvas.imgLoader(global.pack[i].image);
        },
    },
    style: {
        tw: 30, th: 30,
        tx: function () {
            return 40 + i % 8 * 36;
        },
        ty: function () {
            return 31 + Math.floor(i / 8) * 32;
        },
        locate: 'center',
    },
    events: {
        mousemove: function (e) {
            if(Global.pack [I]) {// equipDetail module is responsible for displaying the mouse pointing to the article's equipDetail.show(global.pack[I], e);return! global.hanging.active; }return false;
        },
        mouseout: function() {// Equip detail.hide ();return true;
        },
        click: function() {// Tell the hang module what item is clicked.$sprite: this,
                type: 'pack',
                index: i,
            });
            return true;
        },
        dblclick: function (e) {
            bottomHang.cancel();
            equipDetail.hide();

            useItem(i);

            return true; }}});Copy the code

Since the backpack can change from moment to moment, img is a function that dynamically returns the result. Function () {return 1; function () {return 1; })() The difference in consumption performance is small enough to be ignored.

In style, 40 items are arranged in 8×5. The numbers 40, 31 and 32 are measured from the material map of the backpack. Each grid is 30×30 in size. Mir II also has 6 shortcut bars (hanging on the bottom UI), which were added in a similar way and omitted here. Note, however, that you can’t omit the width and height in the style of each grid, because when img is empty, there needs to be an area of the object in order to capture the event. If the width and height are not specified, clicking a box with no items will not trigger any events. We place an item on a space bar that is needed to capture events.

For each grid, if there is an item in that grid when the mouse moves over it, it needs to display the float of the item’s information. If you click on an item, make the image of the item move with the mouse (the player picks up the item). These two pieces of logic are more complicated, so we write separate modules to take care of them.

Double-click a cell, and you do three things: hide the message float, unpick the item, and use the item (send the request to the server). In legend of Mir II, players are allowed to double click on item B while holding item A (but can’t use item A while holding item A, because you can’t click item A after picking up item A). To achieve complete consistency, you can remove bottomhang. cancel and add the logic that “when you click on a grid, you cannot use an item that is already in your hand.”

This piece does not have too much technical content, as long as the module is removed clean, only the code code to write logic, no longer repeat.

Next we start with the Hang module, where “the player clicks to pick up item A from the backpack and clicks on another item B to switch the location of the two items”. To be clear, from a code point of view, there is no difference between “putting an item in a space child” and “swapping two items”, because the former can be considered an exchange of items and space children. We just need to pass the index I and j of the two item cells to the server.

The general logic is as follows:

// hang.js

const hang = {};

hang.isHanging = false;
hang.index = -1;
hang.lastType = ' ';
hang.$view = UI.add({
    name: 'hangView', style: {}, zIndex: number. MAX_SAFE_INTEGER}); hang.start =function ({$sprite.type, index}) {
    if(! this.isHanging) { this.isHanging =true;
        this.index = index;
        this.lastType = type;
        this.$view.content.img = $sprite.content.img;
        this.$view.style = {tx: () => global.mouse. }else{// Only the last click and the current click are listed hereif (type= = ='pack' && this.lastType === 'pack') {
            this.isHanging = false; // Suppose toServer is a method that sends socket messages to the server.'PACK_CHANGE', hang.index, index); }}}; hang.cancel =function () {
    this.isHanging = false;
    delete this.$view.content.img;
};

export default hang;
Copy the code

First, the hang module has an object, $View, that hangs in the UI layer. When clicking on an item in the backpack, pass the img of the item to display, with the $view following the mouse pointer. (Of course, you also need to hide the item in your backpack, which won’t be described here.)

When cancel is called, kill the img in the $view (and also the unverbose logic of “hiding the item in the backpack”). In this way, you can click the left button and “pick up the item”. If an item has been picked up, the toServer method is called to send the index of both items to the server.

Array [I]=[array[j], array[j]=array[I]][0]. (Of course, if you’re working with the shortcut bar, you need to check the type of item, because only drugs and scrolls can fit in these places. I won’t repeat it here.)

Finally, the server pushes the new array to the client, which then updates it. Looks like we’re done, huh?

No! If there is network lag, it is likely that the player will want to swap the location of item A and item B, and then drop item B. However, due to network problems, the exchange was not completed, and the discard instruction had already been issued. So the player throws item A. Maybe object A is A priceless treasure.

How to avoid such a case? First, players can identify what to lose based on “pictures of items in the backpack.” What the player must not accept is that they pick item B, throw it away, and it becomes item A. Even if a discard fails, a discard is better than a bad execution.

So, we need to solve this problem with the ID of the item. When a player drops an item, we record the “ID of the item moving with the mouse” and send it to the server so that even if the client renders the item list, even if the index order is out of order due to delay, the player will not mistakenly manipulate another item. Of course, it is safer for the server to check again with the index and item ID.

In this way, we can update the client array as soon as the player operates, and when the server responds successfully, we can return the new array to the client (of course, we can only return the changed part or the result of the operation to save the size of the data transfer). Of course, ideally the two arrays are the same. If they are different, we replace the client array with the server array. The same is true for games where the user’s actions are undone due to poor network.

In this way, the hang module implements the exchange of two items in the backpack. As for the linkage between knapsack and other dialogs, such as putting the image frequency in the knapsack into the character’s equipment slot, it can be realized by logically complementing hang.

As for the floating layer that displays the object information, the logic is similar and will not be repeated here. Some of the issues mentioned above, such as placing skills against packs, will be covered in a later section.

Figures in the UI

Once you understand the knapsack, the implementation of the character is relatively simple.

There are two arrows on the left side of the character UI, which can switch between displaying equipment, status, skills, etc. So what we’re going to do is we’re going to cut out the outline of the UI, and then we’re going to cut out each panel, and we’re going to splice it together. As follows:

Then use Easycanvas library to add a parent element as the frame, and fill the parent element with several children. We use a variable to control which panel is currently displayed:

var $role = UI.add({
    name: 'role'// role is the meaning of the role content: {img:'role.png'}, // events will be mentioned later});$role.add({
    name: 'role-equip'// The first page is content: {img:'roleEquip.jpg'}, style:functionVisible: () => role.index === 0}});$role.add({
    name: 'role-state', // The second page is the character status......Copy the code

Then, in the same way that we added grids to knapsacks, we can bind several grids of the character’s equipment to an array or object. The properties on the second page can take the form of a string written on the image. There are not many dry goods and I won’t repeat them here.

So, how to listen for “players take an item from the backpack UI to the equipment slot in the character UI”?

In the first version of the game, I only attached a “double click, send a request to server” event to items in the backpack, and players also wore items in the backpack by double-clicking on them (yes, it was officially possible to do that too). I was going to be lazy and not do the linkage logic for the two UI dialogs, but it turned out to be unavoidable because there would be a warehouse UI in the back and players would have to move items manually. I think it would be considered “anti-human” if you had to double-click an item to access it.

So, I also tied a click event to each grid of the character’s gear. Remember the hang module in the backpack UI? Click on the character’s gear grid to invoke the hang module as well. When we find an item from a backpack in the hang module, click on the character’s equipment to invoke the “Use equipment” command.

So, the processing logic of click events bound to each grid in character equipment is:

  • If the hang module already has an active “item from backpack UI”, try wearing it. (The server finds that there is already a device at this location and performs “Remove The device” first.)

  • If the hang module is idle and the grid is already equipped, throw it into the hang (the user picks up the equipment they are wearing). Also, add an event for clicking the backpack grid: If you find an item in the hang from the character UI, perform “Unequip”.

  • If the hang module already has an active “UI item from the character”, tell the server that I want to swap gear (e.g. left and right gloves) between the two. Of course, the server will check to see if it is interchangeable, such as not putting shoes on your head.

Similarly, each time the server finishes processing, it pushes the data used by the character UI and the updated data in the backpack UI to the client browser for update. Of course, the equipment grid of the character UI also needs to be bound with mouse movement to evoke the floating layer and display equipment information. The entire character UI code is large, but are logical code, there is no bright spot, omitted in this article. Just do a good module encapsulation, write the common logic to the common module.

5. Implementation of Sprite layer

The Sprite layer consists of characters, animals (NPCS, monsters, scene decorations), skills, and other core elements. As mentioned in the introduction, FPS for this tier should be at least 40.

People move

The data logic of character movement

First, the movement of the characters will interact with the ground. Character running modifies the X and Y coordinates in the Global data, triggering the translation effect of the ground. The following two points are involved:

  • When the player controls the character’s movement, whether it is normal or blocked by obstacles, this decision is made on the client side. If you’re doing this on the server side, each step of the run will send a request to the server, and the server will return a successful response, not to mention whether the network latency will cause users to feel that the operation is not smooth, but the frequency of this trigger alone is enough to overwhelm the server. Client-side games typically store which parts of the map are passable in a file that can be downloaded locally for parsing when the player installs the game. In web games, each time a user accesses a map or block, the server sends data (large arrays) for the current map or block. Of course, this data is best done in the browser cache (localStorage), after all, a game can not often change the map.

  • The client continuously reports the coordinates to the server, which processes them and then continuously distributes them to other players. This reporting interval should not be too long. If player A reports every second, player A will always see player B as player B was one second ago. Generally speaking, 0.5 seconds is not acceptable. More than ten years ago, I went to Internet cafe to play with my friends. We ran together. In his screen, he ran a little ahead of me, and I ran a little ahead of me on my screen. Of course, as long as the difference is not much, there will be no problem. (How much is “not much”? It depends on whether the distance error affects the outcome of the release skill. More on that later.)

So how do you prevent players from tampering with the data to achieve “floating” cheating? Let’s say (200, 300) is a pool, and no one can get there. But I told the server in the network request, “I’m at (200, 300), bite me.”

A simple way to do this is to determine on the server whether the coordinate point is reachable, and if not, push a message to the client and ask the client to refresh the position (the player will feel stuck and the character will bounce back). At the same time, we don’t save the invalid data so that other users don’t see it (other players don’t have to see me running into the pool and then bouncing back). All the server has to do is record the facts and state the facts, not accept all the information the player reports. Suppose someone attacks me on the shore, then from the server’s point of view, the attack is valid. As for the cheaters to see “oneself in the pool, incredibly still can be cut”, nothing, so, say! There is no need to write too much compatibility logic for a cheating user, because there is no need to provide a good game experience for such a user.

A more advanced approach would be to determine on the server whether the point is passable, and then whether the player is likely to reach it in time. For example, if someone reported that they were in (100,100) one second and in (900,900) the next, there must be something wrong. We divide the distance by the reporting interval and compare it to the player’s speed. Of course, there should be some redundancy, because players may be unstable network, reported some jitter frequency, so it is normal to calculate the speed of individual time period is faster. From this, we also know, in the plug-in of a certain online game, why open 1.1 times the speed is generally no problem, open 1.5 times the speed will frequently drop the line. Because the server is set with 10% redundancy. Of course, it is possible to identify players who “creep a little further every second” by judging the total distance they travel over N consecutive seconds.

Alternatively, we can encrypt the reported coordinates, or report additional information such as the user’s mouse movement track, to identify whether the operation is legitimate. But doing so just raises the bar for cheating and doesn’t prevent all cases, even if we generate keys dynamically. After all, many online games have automatic running, as long as it doesn’t harm the interests of other players.

View logic for character movement

(Due to the length of the content, other content is temporarily placed in the Github wiki)