• Web学习笔记-中期项目(拳皇)


    1. 项目原理

    游戏中一个物体运动的原理是浏览器每秒钟刷新60次,每次我们单独计算这个物体新的位置,然后把他刷新出来,这样最终人眼看起来就是移动的效果。

    对于二维的移动,我们一般抽象出某个点比如左上角的坐标 ( x , y ) (x,y) (x,y),并记下物体的宽高 w , h w,h w,h和沿 x , y x,y x,y方向的速度 v x , v y v_x,v_y vx,vy,加入物体在水平方向上匀速运动,那么位移就为: x = x 0 + v x t x=x_0+v_xt x=x0+vxt

    2. 基础文件

    首先我们创建主界面index.html以及基础CSS、JS文件base.cssbase.js。然后设置好主界面的大小和背景(此时JS文件没有功能,只用于测试):

    DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>King of Fighterstitle>
    
        <link rel="stylesheet" href="/static/css/base.css">
        <script src="/static/js/jquery-3.6.1.min.js">script>
    head>
    
    <body>
        <div id="kof">div>
    
        <script type="module">
            import { KOF } from '/static/js/base.js';
    
            let kof = new KOF('kof');
        script>
    body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    #kof {
        width: 1280px;
        height: 720px;
    
        background-image: url('/static/images/background/1.gif');
        background-size: 100% 100%;
        background-position: top;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    class KOF {
        constructor(id) {
            this.$kof = $('#' + id);
            console.log(this.$kof);
        }
    }
    
    export {
        KOF
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3. ac_game_object框架

    项目中的背景、两个玩家一共三个元素,对于这三个元素我们都需要实现每秒钟刷新60次,所以我们可以让这三个元素继承至同一个元素,我们在/static/js中创建一个新的文件夹ac_game_object,并在该文件夹创建base.js(为了区分之后称为ac_game_object/base.js)。该文件框架代码如下:

    let AC_GAME_OBJECTS = [];
    
    class AcGameObject {
        constructor() {
            AC_GAME_OBJECTS.push(this);
    
            this.timedelta = 0;  // 存储当前这帧距离上一帧的时间间隔
            this.has_call_start = false;  // 表示当前对象是否执行过start()
        }
    
        start() {  // 初始化执行一次
    
        }
    
        update() {  // 除第一帧外每帧执行一次
    
        }
    
        destroy() {  // 删除当前对象
            for (let i in AC_GAME_OBJECTS) {
                if (AC_GAME_OBJECTS[i] === this) {
                    AC_GAME_OBJECTS.splice(i, 1);
                    break;
                }
            }
        }
    }
    
    let last_timestamp;  // 记录上一帧在什么时间执行
    
    let AC_GAME_OBJECTS_FRAME = (timestamp) => {
        for (let obj of AC_GAME_OBJECTS) {
            if (!obj.has_call_start) {
                obj.start();
                obj.has_call_start = true;
            } else {
                obj.timedelta = timestamp - last_timestamp;
                obj.update();
            }
        }
    
        last_timestamp = timestamp;
    
        requestAnimationFrame(AC_GAME_OBJECTS_FRAME);
    }
    
    requestAnimationFrame(AC_GAME_OBJECTS_FRAME);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    4. 游戏地图与玩家模型的创建

    使用canvas设计基本的地图和玩家,2D平面一般使用一个矩形来表示一个玩家模型所占的区域,/static/js/game_map/base.js代码如下:

    import { AcGameObject } from '/static/js/ac_game_object/base.js'
    
    class GameMap extends AcGameObject {
        constructor(root) {
            super();
    
            this.root = root;
            this.$canvas = $('');
            this.ctx = this.$canvas[0].getContext('2d');
            this.root.$kof.append(this.$canvas);
            this.$canvas.focus();
        }
    
        start() {
    
        }
    
        update() {
            this.render();
        }
    
        render() {  // 渲染函数
            // 每一帧需要清空地图,不然看到的效果就不是物体在移动,而是拖出一条线
            this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
            this.ctx.fillStyle = 'black';
            this.ctx.fillRect(0, 0, this.$canvas.width(), this.$canvas.height());
        }
    }
    
    export {
        GameMap
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    /static/js/player/base.js代码如下:

    import { AcGameObject } from "/static/js/ac_game_object/base.js";
    
    class Player extends AcGameObject {
        constructor(root, info) {
            super();
    
            this.root = root;
            this.id = info.id;
            this.x = info.x;
            this.y = info.y;
            this.width = info.width;
            this.height = info.height;
            this.color = info.color;
    
            this.vx = 0;
            this.vy = 0;
    
            this.speedx = 350;  // 水平速度
            this.speedy = -1400;  // 跳起的初始速度
    
            this.ctx = this.root.game_map.ctx;
        }
    
        start() {
    
        }
    
        update() {
            this.render();
        }
    
        render() {
            this.ctx.fillStyle = this.color;
            this.ctx.fillRect(this.x, this.y, this.width, this.height);
        }
    }
    
    export {
        Player
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    主文件base.js代码如下:

    import { GameMap } from '/static/js/game_map/base.js';
    import { Player } from '/static/js/player/base.js';
    
    class KOF {
        constructor(id) {
            this.$kof = $('#' + id);
    
            this.game_map = new GameMap(this);
            this.players = [
                new Player(this, {
                    id: 0,
                    x: 200,
                    y: this.$kof.height() - 200,  // 之后需要改成0,然后设置角色初始状态为跳跃,根据重力让其自由落体
                    width: 120,
                    height: 200,
                    color: 'blue'
                }),
                new Player(this, {
                    id: 1,
                    x: 900,
                    y: this.$kof.height() - 200,  // 之后需要改成0,然后设置角色初始状态为跳跃,根据重力让其自由落体
                    width: 120,
                    height: 200,
                    color: 'red'
                }),
            ]
        }
    }
    
    export {
        KOF
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    此时的效果如下图所示:

    在这里插入图片描述

    5. 角色状态的实现

    由于游戏中角色有静止不动、移动、跳跃等多种状态,因此我们需要使用状态机加以区分,先考虑静止,移动(包括左右移动),跳跃这三种状态,我们分别用0,1,3表示这三个状态,且设定在跳跃状态时无法进行其它操作,状态机如下图所示:

    在这里插入图片描述

    首先我们需要实现按住某个键角色连续移动的功能,如果只靠keydown判断那么是一连串离散的键值(例如按住某个键后第一至第二下输入有明显间隔),实现控制角色的类/static/js/controller/base/js代码如下:

    class Controller {  // 用于读取键盘输入
        constructor($canvas) {
            this.$canvas = $canvas;
    
            this.pressed_keys = new Set();
            this.start();
        }
    
        start() {
            let outer = this;
            this.$canvas.on('keydown', function (e) {
                outer.pressed_keys.add(e.key);
            });
    
            this.$canvas.on('keyup', function (e) {
                outer.pressed_keys.delete(e.key);
            });
        }
    }
    
    export {
        Controller
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    然后在GameMap类中创建一个Controller类,然后实现角色的基本操作逻辑,/static/js/player/base.js代码如下:

    import { AcGameObject } from "/static/js/ac_game_object/base.js";
    
    class Player extends AcGameObject {
        constructor(root, info) {
            super();
    
            this.root = root;
            this.id = info.id;
            this.x = info.x;
            this.y = info.y;
            this.width = info.width;
            this.height = info.height;
            this.color = info.color;
    
            this.direction = 1;  // 角色的方向,正方向为1,反方向为-1
    
            this.vx = 0;  // 当前水平速度
            this.vy = 0;  // 当前垂直速度
    
            this.speedx = 350;  // 水平移动速度
            this.speedy = -1400;  // 跳起的初始速度
    
            this.gravity = 25;  // 重力
    
            this.ctx = this.root.game_map.ctx;
    
            this.pressed_keys = this.root.game_map.controller.pressed_keys;
    
            this.status = 3;  // 0: idle,1: forward,2: backward,3: jump,4: attack,5: be attacked,6: die
            this.animations = new Map();  // 表示每个状态的动作
        }
    
        start() {
    
        }
    
        update_move() {
            this.vy += this.gravity;
    
            this.x += this.vx * this.timedelta / 1000;
            this.y += this.vy * this.timedelta / 1000;
    
            if (this.y > 450) {  // 落到地上时停止下落
                this.y = 450;
                this.vy = 0;
                if (this.status === 3) this.status = 0;  // 只有之前是跳跃状态才需要从跳跃状态转变为静止状态
            }
    
            if (this.x < 0) {  // 左右边界判断
                this.x = 0;
            } else if (this.x + this.width > this.root.game_map.$canvas.width()) {
                this.x = this.root.game_map.$canvas.width() - this.width;
            }
        }
    
        update_control() {
            let w, a, d, j;  // 表示这些键是否按住
            if (this.id === 0) {
                w = this.pressed_keys.has('w');
                a = this.pressed_keys.has('a');
                d = this.pressed_keys.has('d');
                j = this.pressed_keys.has('j');
            } else {
                w = this.pressed_keys.has('ArrowUp');
                a = this.pressed_keys.has('ArrowLeft');
                d = this.pressed_keys.has('ArrowRight');
                j = this.pressed_keys.has('1');
            }
    
            if (this.status === 0 || this.status === 1) {  /// 假设角色在跳跃状态无法操控
                if (w) {  // 跳跃有向右跳,垂直跳和向左跳
                    if (d) {
                        this.vx = this.speedx;
                    } else if (a) {
                        this.vx = -this.speedx;
                    }
                    else {
                        this.vx = 0;
                    }
                    this.vy = this.speedy;
                    this.status = 3;
                    this.frame_current_cnt = 0;  // 从第0帧开始渲染
                } else if (j) {
                    this.status = 4;
                    this.vx = 0;
                    this.frame_current_cnt = 0;  // 从第0帧开始渲染
                } else if (d) {
                    this.vx = this.speedx;
                    this.status = 1;
                } else if (a) {
                    this.vx = -this.speedx;
                    this.status = 1;
                } else {
                    this.vx = 0;
                    this.status = 0;
                }
            }
        }
    
        update() {
            this.update_control();
            this.update_move();
    
            this.render();
        }
    
        render() {
            this.ctx.fillStyle = this.color;
            this.ctx.fillRect(this.x, this.y, this.width, this.height);
        }
    }
    
    export {
        Player
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115

    效果如下:

    在这里插入图片描述

    6. 角色基础状态动画实现

    首先在网上找到在canvas中加入.gif文件的工具JS代码(点此跳转),/static/js/utils/gif.js文件代码如下:

    const GIF = function () {
        // **NOT** for commercial use.
        var timerID;                          // timer handle for set time out usage
        var st;                               // holds the stream object when loading.
        var interlaceOffsets = [0, 4, 2, 1]; // used in de-interlacing.
        var interlaceSteps = [8, 8, 4, 2];
        var interlacedBufSize;  // this holds a buffer to de interlace. Created on the first frame and when size changed
        var deinterlaceBuf;
        var pixelBufSize;    // this holds a buffer for pixels. Created on the first frame and when size changed
        var pixelBuf;
        const GIF_FILE = { // gif file data headers
            GCExt: 0xF9,
            COMMENT: 0xFE,
            APPExt: 0xFF,
            UNKNOWN: 0x01, // not sure what this is but need to skip it in parser
            IMAGE: 0x2C,
            EOF: 59,   // This is entered as decimal
            EXT: 0x21,
        };
        // simple buffered stream used to read from the file 
        var Stream = function (data) {
            this.data = new Uint8ClampedArray(data);
            this.pos = 0;
            var len = this.data.length;
            this.getString = function (count) { // returns a string from current pos of len count
                var s = "";
                while (count--) { s += String.fromCharCode(this.data[this.pos++]) }
                return s;
            };
            this.readSubBlocks = function () { // reads a set of blocks as a string
                var size, count, data = "";
                do {
                    count = size = this.data[this.pos++];
                    while (count--) { data += String.fromCharCode(this.data[this.pos++]) }
                } while (size !== 0 && this.pos < len);
                return data;
            }
            this.readSubBlocksB = function () { // reads a set of blocks as binary
                var size, count, data = [];
                do {
                    count = size = this.data[this.pos++];
                    while (count--) { data.push(this.data[this.pos++]); }
                } while (size !== 0 && this.pos < len);
                return data;
            }
        };
        // LZW decoder uncompressed each frames pixels
        // this needs to be optimised.
        // minSize is the min dictionary as powers of two
        // size and data is the compressed pixels
        function lzwDecode(minSize, data) {
            var i, pixelPos, pos, clear, eod, size, done, dic, code, last, d, len;
            pos = pixelPos = 0;
            dic = [];
            clear = 1 << minSize;
            eod = clear + 1;
            size = minSize + 1;
            done = false;
            while (!done) { // JavaScript optimisers like a clear exit though I never use 'done' apart from fooling the optimiser
                last = code;
                code = 0;
                for (i = 0; i < size; i++) {
                    if (data[pos >> 3] & (1 << (pos & 7))) { code |= 1 << i }
                    pos++;
                }
                if (code === clear) { // clear and reset the dictionary
                    dic = [];
                    size = minSize + 1;
                    for (i = 0; i < clear; i++) { dic[i] = [i] }
                    dic[clear] = [];
                    dic[eod] = null;
                } else {
                    if (code === eod) { done = true; return }
                    if (code >= dic.length) { dic.push(dic[last].concat(dic[last][0])) }
                    else if (last !== clear) { dic.push(dic[last].concat(dic[code][0])) }
                    d = dic[code];
                    len = d.length;
                    for (i = 0; i < len; i++) { pixelBuf[pixelPos++] = d[i] }
                    if (dic.length === (1 << size) && size < 12) { size++ }
                }
            }
        };
        function parseColourTable(count) { // get a colour table of length count  Each entry is 3 bytes, for RGB.
            var colours = [];
            for (var i = 0; i < count; i++) { colours.push([st.data[st.pos++], st.data[st.pos++], st.data[st.pos++]]) }
            return colours;
        }
        function parse() {        // read the header. This is the starting point of the decode and async calls parseBlock
            var bitField;
            st.pos += 6;
            gif.width = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            gif.height = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            bitField = st.data[st.pos++];
            gif.colorRes = (bitField & 0b1110000) >> 4;
            gif.globalColourCount = 1 << ((bitField & 0b111) + 1);
            gif.bgColourIndex = st.data[st.pos++];
            st.pos++;                    // ignoring pixel aspect ratio. if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
            if (bitField & 0b10000000) { gif.globalColourTable = parseColourTable(gif.globalColourCount) } // global colour flag
            setTimeout(parseBlock, 0);
        }
        function parseAppExt() { // get application specific data. Netscape added iterations and terminator. Ignoring that
            st.pos += 1;
            if ('NETSCAPE' === st.getString(8)) { st.pos += 8 }  // ignoring this data. iterations (word) and terminator (byte)
            else {
                st.pos += 3;            // 3 bytes of string usually "2.0" when identifier is NETSCAPE
                st.readSubBlocks();     // unknown app extension
            }
        };
        function parseGCExt() { // get GC data
            var bitField;
            st.pos++;
            bitField = st.data[st.pos++];
            gif.disposalMethod = (bitField & 0b11100) >> 2;
            gif.transparencyGiven = bitField & 0b1 ? true : false; // ignoring bit two that is marked as  userInput???
            gif.delayTime = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            gif.transparencyIndex = st.data[st.pos++];
            st.pos++;
        };
        function parseImg() {                           // decodes image data to create the indexed pixel image
            var deinterlace, frame, bitField;
            deinterlace = function (width) {                   // de interlace pixel data if needed
                var lines, fromLine, pass, toline;
                lines = pixelBufSize / width;
                fromLine = 0;
                if (interlacedBufSize !== pixelBufSize) {      // create the buffer if size changed or undefined.
                    deinterlaceBuf = new Uint8Array(pixelBufSize);
                    interlacedBufSize = pixelBufSize;
                }
                for (pass = 0; pass < 4; pass++) {
                    for (toLine = interlaceOffsets[pass]; toLine < lines; toLine += interlaceSteps[pass]) {
                        deinterlaceBuf.set(pixelBuf.subarray(fromLine, fromLine + width), toLine * width);
                        fromLine += width;
                    }
                }
            };
            frame = {}
            gif.frames.push(frame);
            frame.disposalMethod = gif.disposalMethod;
            frame.time = gif.length;
            frame.delay = gif.delayTime * 10;
            gif.length += frame.delay;
            if (gif.transparencyGiven) { frame.transparencyIndex = gif.transparencyIndex }
            else { frame.transparencyIndex = undefined }
            frame.leftPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            frame.topPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            frame.width = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            frame.height = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
            bitField = st.data[st.pos++];
            frame.localColourTableFlag = bitField & 0b10000000 ? true : false;
            if (frame.localColourTableFlag) { frame.localColourTable = parseColourTable(1 << ((bitField & 0b111) + 1)) }
            if (pixelBufSize !== frame.width * frame.height) { // create a pixel buffer if not yet created or if current frame size is different from previous
                pixelBuf = new Uint8Array(frame.width * frame.height);
                pixelBufSize = frame.width * frame.height;
            }
            lzwDecode(st.data[st.pos++], st.readSubBlocksB()); // decode the pixels
            if (bitField & 0b1000000) {                        // de interlace if needed
                frame.interlaced = true;
                deinterlace(frame.width);
            } else { frame.interlaced = false }
            processFrame(frame);                               // convert to canvas image
        };
        function processFrame(frame) { // creates a RGBA canvas image from the indexed pixel data.
            var ct, cData, dat, pixCount, ind, useT, i, pixel, pDat, col, frame, ti;
            frame.image = document.createElement('canvas');
            frame.image.width = gif.width;
            frame.image.height = gif.height;
            frame.image.ctx = frame.image.getContext("2d");
            ct = frame.localColourTableFlag ? frame.localColourTable : gif.globalColourTable;
            if (gif.lastFrame === null) { gif.lastFrame = frame }
            useT = (gif.lastFrame.disposalMethod === 2 || gif.lastFrame.disposalMethod === 3) ? true : false;
            if (!useT) { frame.image.ctx.drawImage(gif.lastFrame.image, 0, 0, gif.width, gif.height) }
            cData = frame.image.ctx.getImageData(frame.leftPos, frame.topPos, frame.width, frame.height);
            ti = frame.transparencyIndex;
            dat = cData.data;
            if (frame.interlaced) { pDat = deinterlaceBuf }
            else { pDat = pixelBuf }
            pixCount = pDat.length;
            ind = 0;
            for (i = 0; i < pixCount; i++) {
                pixel = pDat[i];
                col = ct[pixel];
                if (ti !== pixel) {
                    dat[ind++] = col[0];
                    dat[ind++] = col[1];
                    dat[ind++] = col[2];
                    dat[ind++] = 255;      // Opaque.
                } else
                    if (useT) {
                        dat[ind + 3] = 0; // Transparent.
                        ind += 4;
                    } else { ind += 4 }
            }
            frame.image.ctx.putImageData(cData, frame.leftPos, frame.topPos);
            gif.lastFrame = frame;
            if (!gif.waitTillDone && typeof gif.onload === "function") { doOnloadEvent() }// if !waitTillDone the call onload now after first frame is loaded
        };
        // **NOT** for commercial use.
        function finnished() { // called when the load has completed
            gif.loading = false;
            gif.frameCount = gif.frames.length;
            gif.lastFrame = null;
            st = undefined;
            gif.complete = true;
            gif.disposalMethod = undefined;
            gif.transparencyGiven = undefined;
            gif.delayTime = undefined;
            gif.transparencyIndex = undefined;
            gif.waitTillDone = undefined;
            pixelBuf = undefined; // dereference pixel buffer
            deinterlaceBuf = undefined; // dereference interlace buff (may or may not be used);
            pixelBufSize = undefined;
            deinterlaceBuf = undefined;
            gif.currentFrame = 0;
            if (gif.frames.length > 0) { gif.image = gif.frames[0].image }
            doOnloadEvent();
            if (typeof gif.onloadall === "function") {
                (gif.onloadall.bind(gif))({ type: 'loadall', path: [gif] });
            }
            if (gif.playOnLoad) { gif.play() }
        }
        function canceled() { // called if the load has been cancelled
            finnished();
            if (typeof gif.cancelCallback === "function") { (gif.cancelCallback.bind(gif))({ type: 'canceled', path: [gif] }) }
        }
        function parseExt() {              // parse extended blocks
            const blockID = st.data[st.pos++];
            if (blockID === GIF_FILE.GCExt) { parseGCExt() }
            else if (blockID === GIF_FILE.COMMENT) { gif.comment += st.readSubBlocks() }
            else if (blockID === GIF_FILE.APPExt) { parseAppExt() }
            else {
                if (blockID === GIF_FILE.UNKNOWN) { st.pos += 13; } // skip unknow block
                st.readSubBlocks();
            }
    
        }
        function parseBlock() { // parsing the blocks
            if (gif.cancel !== undefined && gif.cancel === true) { canceled(); return }
    
            const blockId = st.data[st.pos++];
            if (blockId === GIF_FILE.IMAGE) { // image block
                parseImg();
                if (gif.firstFrameOnly) { finnished(); return }
            } else if (blockId === GIF_FILE.EOF) { finnished(); return }
            else { parseExt() }
            if (typeof gif.onprogress === "function") {
                gif.onprogress({ bytesRead: st.pos, totalBytes: st.data.length, frame: gif.frames.length });
            }
            setTimeout(parseBlock, 0); // parsing frame async so processes can get some time in.
        };
        function cancelLoad(callback) { // cancels the loading. This will cancel the load before the next frame is decoded
            if (gif.complete) { return false }
            gif.cancelCallback = callback;
            gif.cancel = true;
            return true;
        }
        function error(type) {
            if (typeof gif.onerror === "function") { (gif.onerror.bind(this))({ type: type, path: [this] }) }
            gif.onload = gif.onerror = undefined;
            gif.loading = false;
        }
        function doOnloadEvent() { // fire onload event if set
            gif.currentFrame = 0;
            gif.nextFrameAt = gif.lastFrameAt = new Date().valueOf(); // just sets the time now
            if (typeof gif.onload === "function") { (gif.onload.bind(gif))({ type: 'load', path: [gif] }) }
            gif.onerror = gif.onload = undefined;
        }
        function dataLoaded(data) { // Data loaded create stream and parse
            st = new Stream(data);
            parse();
        }
        function loadGif(filename) { // starts the load
            var ajax = new XMLHttpRequest();
            ajax.responseType = "arraybuffer";
            ajax.onload = function (e) {
                if (e.target.status === 404) { error("File not found") }
                else if (e.target.status >= 200 && e.target.status < 300) { dataLoaded(ajax.response) }
                else { error("Loading error : " + e.target.status) }
            };
            ajax.open('GET', filename, true);
            ajax.send();
            ajax.onerror = function (e) { error("File error") };
            this.src = filename;
            this.loading = true;
        }
        function play() { // starts play if paused
            if (!gif.playing) {
                gif.paused = false;
                gif.playing = true;
                playing();
            }
        }
        function pause() { // stops play
            gif.paused = true;
            gif.playing = false;
            clearTimeout(timerID);
        }
        function togglePlay() {
            if (gif.paused || !gif.playing) { gif.play() }
            else { gif.pause() }
        }
        function seekFrame(frame) { // seeks to frame number.
            clearTimeout(timerID);
            gif.currentFrame = frame % gif.frames.length;
            if (gif.playing) { playing() }
            else { gif.image = gif.frames[gif.currentFrame].image }
        }
        function seek(time) { // time in Seconds  // seek to frame that would be displayed at time
            clearTimeout(timerID);
            if (time < 0) { time = 0 }
            time *= 1000; // in ms
            time %= gif.length;
            var frame = 0;
            while (time > gif.frames[frame].time + gif.frames[frame].delay && frame < gif.frames.length) { frame += 1 }
            gif.currentFrame = frame;
            if (gif.playing) { playing() }
            else { gif.image = gif.frames[gif.currentFrame].image }
        }
        function playing() {
            var delay;
            var frame;
            if (gif.playSpeed === 0) {
                gif.pause();
                return;
            } else {
                if (gif.playSpeed < 0) {
                    gif.currentFrame -= 1;
                    if (gif.currentFrame < 0) { gif.currentFrame = gif.frames.length - 1 }
                    frame = gif.currentFrame;
                    frame -= 1;
                    if (frame < 0) { frame = gif.frames.length - 1 }
                    delay = -gif.frames[frame].delay * 1 / gif.playSpeed;
                } else {
                    gif.currentFrame += 1;
                    gif.currentFrame %= gif.frames.length;
                    delay = gif.frames[gif.currentFrame].delay * 1 / gif.playSpeed;
                }
                gif.image = gif.frames[gif.currentFrame].image;
                timerID = setTimeout(playing, delay);
            }
        }
        var gif = {                      // the gif image object
            onload: null,       // fire on load. Use waitTillDone = true to have load fire at end or false to fire on first frame
            onerror: null,       // fires on error
            onprogress: null,       // fires a load progress event
            onloadall: null,       // event fires when all frames have loaded and gif is ready
            paused: false,      // true if paused
            playing: false,      // true if playing
            waitTillDone: true,       // If true onload will fire when all frames loaded, if false, onload will fire when first frame has loaded
            loading: false,      // true if still loading
            firstFrameOnly: false,      // if true only load the first frame
            width: null,       // width in pixels
            height: null,       // height in pixels
            frames: [],         // array of frames
            comment: "",         // comments if found in file. Note I remember that some gifs have comments per frame if so this will be all comment concatenated
            length: 0,          // gif length in ms (1/1000 second)
            currentFrame: 0,          // current frame. 
            frameCount: 0,          // number of frames
            playSpeed: 1,          // play speed 1 normal, 2 twice 0.5 half, -1 reverse etc...
            lastFrame: null,       // temp hold last frame loaded so you can display the gif as it loads
            image: null,       // the current image at the currentFrame
            playOnLoad: true,       // if true starts playback when loaded
            // functions
            load: loadGif,    // call this to load a file
            cancel: cancelLoad, // call to stop loading
            play: play,       // call to start play
            pause: pause,      // call to pause
            seek: seek,       // call to seek to time
            seekFrame: seekFrame,  // call to seek to frame
            togglePlay: togglePlay, // call to toggle play and pause state
        };
        return gif;
    }
    
    export {
        GIF
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315
    • 316
    • 317
    • 318
    • 319
    • 320
    • 321
    • 322
    • 323
    • 324
    • 325
    • 326
    • 327
    • 328
    • 329
    • 330
    • 331
    • 332
    • 333
    • 334
    • 335
    • 336
    • 337
    • 338
    • 339
    • 340
    • 341
    • 342
    • 343
    • 344
    • 345
    • 346
    • 347
    • 348
    • 349
    • 350
    • 351
    • 352
    • 353
    • 354
    • 355
    • 356
    • 357
    • 358
    • 359
    • 360
    • 361
    • 362
    • 363
    • 364
    • 365
    • 366
    • 367
    • 368
    • 369
    • 370
    • 371
    • 372
    • 373
    • 374
    • 375
    • 376

    创建角色Kyo,将每个动画加载出来,由于有的动画高度不同,因此需要设置不同动画在 y y y轴上的偏移量,且由于动画帧数不多,网页每秒渲染帧数太多,因此需要设置浏览器渲染几帧时再渲染角色的动画,/static/js/player/kyo.js代码如下:

    import { Player } from '/static/js/player/base.js';
    import { GIF } from '/static/js/utils/gif.js';
    
    class Kyo extends Player {
        constructor(root, info) {
            super(root, info);
    
            this.init_animations();
        }
    
        init_animations() {
            let outer = this;
            let offsets = [0, -22, -22, -100, 0, 0, 0];
            for (let i = 0; i < 7; i++) {  // 一共7个动画
                let gif = GIF();
                gif.load(`/static/images/player/kyo/${i}.gif`);
                this.animations.set(i, {
                    gif: gif,
                    frame_cnt: 0,  // 表示gif中的总图片数
                    frame_rate: 12,  // 表示每12帧渲染一次
                    offset_y: offsets[i],  // y方向的偏移量
                    loaded: false,  // 表示是否加载完成
                    scale: 2  // 角色放大倍数
                });
    
                if (i === 3) this.animations.get(i).frame_rate = 10;
    
                gif.onload = function () {
                    let obj = outer.animations.get(i);
                    obj.frame_cnt = gif.frames.length;
                    obj.loaded = true;
                }
            }
        }
    }
    
    export {
        Kyo
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    然后将主JS文件base.js的内容修改为:

    import { GameMap } from '/static/js/game_map/base.js';
    import { Kyo } from '/static/js/player/kyo.js';
    
    class KOF {
        constructor(id) {
            this.$kof = $('#' + id);
    
            this.game_map = new GameMap(this);
            this.players = [
                new Kyo(this, {
                    id: 0,
                    x: 200,
                    y: 0,
                    width: 120,
                    height: 200,
                    color: 'blue'
                }),
                new Kyo(this, {
                    id: 1,
                    x: 900,
                    y: 0,
                    width: 120,
                    height: 200,
                    color: 'red'
                }),
            ]
        }
    }
    
    export {
        KOF
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    然后针对不同的角色状态,设置相应的动画效果,在设置角色朝向的时候,由于动画无法翻转,因此需要将canvas进行翻转,翻转后需要注意调整角色参考坐标的映射,具体操作为先沿canvas中轴对称,然后再减去角色宽度,如下图所示:

    在这里插入图片描述

    此时实现的功能在/static/js/player/base.js核心代码如下:

    class Player extends AcGameObject {
        constructor(root, info) {
            super();
    
            this.direction = 1;  // 角色的方向,正方向为1,反方向为-1
    
            this.ctx = this.root.game_map.ctx;
    
            this.status = 3;  // 0: idle,1: forward,2: backward,3: jump,4: attack,5: be attacked,6: die
            this.animations = new Map();  // 表示每个状态的动作
            this.frame_current_cnt = 0;  // 表示当前记录了多少帧
        }
    
        start() {
    
        }
    
        update_direction() {
            if (this.status === 6) return;  // die后不再改变方向
    
            let players = this.root.players;
            if (players[0] && players[1]) {
                let me = this, you = players[1 - this.id];
                if (me.x < you.x) me.direction = 1;
                else me.direction = -1;
            }
        }
    
        update() {
            this.update_control();
            this.update_direction();
            this.update_move();
    
            this.render();
        }
    
        render() {
            // 测试玩家模型
            // this.ctx.fillStyle = this.color;
            // this.ctx.fillRect(this.x, this.y, this.width, this.height);
    
            let status = this.status;
    
            if (this.status === 1 && this.direction * this.vx < 0) {  // 如果角色方向和水平速度方向乘积为负说明是后退
                status = 2;
            }
    
            let obj = this.animations.get(status);
            if (obj && obj.loaded) {
                if (this.direction > 0) {
                    let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;  // 循环渲染,且控制其不每帧渲染一次,否则动作速度太快
                    let image = obj.gif.frames[k].image;
                    this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);
                } else {  // 当前角色方向为负方向
                    this.ctx.save();
                    this.ctx.scale(-1, 1);  // x轴坐标乘上-1,y轴坐标不变
                    this.ctx.translate(-this.root.game_map.$canvas.width(), 0);
    
                    let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;
                    let image = obj.gif.frames[k].image;
                    this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);
    
                    this.ctx.restore();
                }
            }
    
            // 跳跃和攻击动画结束后应回到静止状态
            if ((status === 3 || status === 4 || status === 5) && this.frame_current_cnt === obj.frame_rate * (obj.frame_cnt - 1)) {
                this.status = 0;
            }
    
            this.frame_current_cnt++;
        }
    }
    
    export {
        Player
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    7. 角色攻击与被攻击状态实现

    我们用一个矩形区域表示角色的挥拳范围,当攻击时我们判断攻击角色的攻击区域和另一名角色的模型矩形区域有交集,那么另一名角色受到攻击,被攻击函数中我们设置相应的被打状态以及血量之类的修改,此时实现的功能在/static/js/player/base.js核心代码如下:

    class Player extends AcGameObject {
        constructor(root, info) {
            super();
    
            this.ctx = this.root.game_map.ctx;
    
            this.status = 3;  // 0: idle,1: forward,2: backward,3: jump,4: attack,5: be attacked,6: die
            this.animations = new Map();  // 表示每个状态的动作
            this.frame_current_cnt = 0;  // 表示当前记录了多少帧
    
            this.hp = 100;
            this.$hp_outer = this.root.$kof.find(`.kof-head>.kof-head-hp-${this.id}>.kof-head-hp-${this.id}-inner>.kof-head-hp-${this.id}-outer`);  // 外层血条
            this.$hp_inner = this.root.$kof.find(`.kof-head>.kof-head-hp-${this.id}>.kof-head-hp-${this.id}-inner`);  // 内层血条
        }
    
        start() {
    
        }
    
        is_attacked() {  // 被攻击
            if (this.status === 6) return;  // die后不再被攻击
    
            this.status = 5;
            this.frame_current_cnt = 0;
    
            this.hp = Math.max(this.hp - 20, 0);
    
            // 使用transition控制血条衰减的速度
            this.$hp_outer.css({
                width: this.$hp_inner.parent().width() * this.hp / 100,
            })
            this.$hp_inner.css({
                width: this.$hp_inner.parent().width() * this.hp / 100,
                transition: '1500ms'
            })
    
            // 使用animate控制血条衰减的速度
            // this.$hp_outer.width(this.$hp_inner.parent().width() * this.hp / 100);
            // this.$hp_inner.animate({
            //     width: this.$hp_inner.parent().width() * this.hp / 100
            // }, 1500);
    
            this.vx = 100 * (-this.direction);  // 向反方向的击退效果
    
            if (this.hp === 0) {
                this.status = 6;
                this.frame_current_cnt = 0;
            }
        }
    
        is_collision(r1, r2) {  // 碰撞检测
            if (Math.max(r1.x1, r2.x1) > Math.min(r1.x2, r2.x2))
                return false;
            if (Math.max(r1.y1, r2.y1) > Math.min(r1.y2, r2.y2))
                return false;
            return true;
        }
    
        update_attack() {
            if (this.status === 4 && this.frame_current_cnt === 38) {  // 攻击动画到第38帧的时候检测碰撞
                let me = this, you = this.root.players[1 - this.id];
    
                let r1;
                if (me.direction > 0) {
                    r1 = {
                        x1: me.x + 120,  // (x1, y1)为攻击区域的左上角坐标
                        y1: me.y + 40,
                        x2: me.x + 120 + 100,  // (x2, y2)为攻击区域的右下角坐标
                        y2: me.y + 40 + 20
                    }
                } else {
                    r1 = {
                        x1: this.x + this.width - 220,
                        y1: me.y + 40,
                        x2: this.x + this.width - 220 + 100,
                        y2: me.y + 40 + 20
                    }
                }
    
                let r2 = {
                    x1: you.x,
                    y1: you.y,
                    x2: you.x + you.width,
                    y2: you.y + you.height
                }
    
                if (this.is_collision(r1, r2)) {
                    you.is_attacked();
                }
            }
        }
    
        update() {
            this.update_control();
            this.update_direction();
            this.update_move();
            this.update_attack();
    
            this.render();
        }
    
        render() {
            // 测试玩家模型
            // this.ctx.fillStyle = this.color;
            // this.ctx.fillRect(this.x, this.y, this.width, this.height);
    
            // 测试出拳碰撞模型
            // if (this.direction > 0) {
            //     this.ctx.fillStyle = 'red';
            //     this.ctx.fillRect(this.x + 120, this.y + 40, 100, 20);
            // } else {
            //     this.ctx.fillStyle = 'red';
            //     this.ctx.fillRect(this.x + this.width - 220, this.y + 40, 100, 20);
            // }
    
            let status = this.status;
    
            if (this.status === 1 && this.direction * this.vx < 0) {  // 如果角色方向和水平速度方向乘积为负说明是后退
                status = 2;
            }
    
            let obj = this.animations.get(status);
            if (obj && obj.loaded) {
                if (this.direction > 0) {
                    let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;  // 循环渲染,且控制其不每帧渲染一次,否则动作速度太快
                    let image = obj.gif.frames[k].image;
                    this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);
                } else {  // 当前角色方向为负方向
                    this.ctx.save();
                    this.ctx.scale(-1, 1);  // x轴坐标乘上-1,y轴坐标不变
                    this.ctx.translate(-this.root.game_map.$canvas.width(), 0);
    
                    let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt;
                    let image = obj.gif.frames[k].image;
                    this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale);
    
                    this.ctx.restore();
                }
            }
    
            // 跳跃和攻击动画结束后应回到静止状态
            if ((status === 3 || status === 4 || status === 5) && this.frame_current_cnt === obj.frame_rate * (obj.frame_cnt - 1)) {
                this.status = 0;
            }
    
            // die的最后一帧后应倒地不起
            if (status === 6 && this.frame_current_cnt === obj.frame_rate * (obj.frame_cnt - 1)) {
                this.frame_current_cnt--;  // 和后面的this.frame_current_cnt++抵消
                this.vx = 0;  // die后不再有击退效果
            }
    
            this.frame_current_cnt++;
        }
    }
    
    export {
        Player
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158

    8. 前端组件的补充及计时结束后双方胜负的判断

    我们需要在页面上加上两个玩家的血条以及计时器,血条的设计需要三层div的设计,最外层的.kof-head-hp-0表示玩家0血条的边框,第二层的.kof-head-hp-0>.kof-head-hp-0-inner表示血条底层的红条,最内层的.kof-head-hp-0>.kof-head-hp-0-inner>.kof-head-hp-0-outer表示血条表层(覆盖在最上面)的黄条,当掉血时,控制黄条以更快的速度衰减,红条以更慢的速度衰减即可。

    代码如下:

    this.$hp_outer = this.root.$kof.find(`.kof-head>.kof-head-hp-${this.id}>.kof-head-hp-${this.id}-inner>.kof-head-hp-${this.id}-outer`);  // 外层血条
    this.$hp_inner = this.root.$kof.find(`.kof-head>.kof-head-hp-${this.id}>.kof-head-hp-${this.id}-inner`);  // 内层血条
    
    // 使用transition控制血条衰减的速度
    this.$hp_outer.css({
        width: this.$hp_inner.parent().width() * this.hp / 100,
    })
    this.$hp_inner.css({
        width: this.$hp_inner.parent().width() * this.hp / 100,
        transition: '1500ms'
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    当倒计时结束时,如果双方血量相同则同时倒地,否则血量少的一方倒地,然后同时更新血条即可,/static/js/game_map/base.js核心代码如下:

    update_hp(player) {
        this.$hp_outer = this.root.$kof.find(`.kof-head>.kof-head-hp-${player.id}>.kof-head-hp-${player.id}-inner>.kof-head-hp-${player.id}-outer`);
        this.$hp_inner = this.root.$kof.find(`.kof-head>.kof-head-hp-${player.id}>.kof-head-hp-${player.id}-inner`);
        this.$hp_outer.css({
            width: this.$hp_inner.parent().width() * player.hp / 100,
        })
        this.$hp_inner.css({
            width: this.$hp_inner.parent().width() * player.hp / 100,
            transition: '1500ms'
        })
    }
    
    update() {
        let [a, b] = this.root.players;
    
        if (this.time_left > 0 && a.status !== 6 && b.status !== 6) {  // 没人die时计时
            this.time_left -= this.timedelta;
        } else if (this.time_left < 0 && this.time_left > -500) {  // 时间结束后血少的玩家die,血相同一起die,只执行一次
            this.time_left = -500;
    
            if (a.hp !== b.hp) {
                let lower = (a.hp > b.hp) ? b : a;
                lower.hp = 0;
                lower.status = 6;
                lower.frame_current_cnt = 0;
    
                this.update_hp(lower);
            } else {
                a.status = b.status = 6;
                a.hp = b.hp = 0;
                a.frame_current_cnt = b.frame_current_cnt = 0;
    
                this.update_hp(a);
                this.update_hp(b);
            }
        }
    
        this.$timer.text(parseInt(this.time_left / 1000));
    
        this.render();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    至此,整个项目实现完毕。

  • 相关阅读:
    动态规划学习1
    如何规避容器内做Java堆dump导致容器崩溃的问题
    Android12版本闹钟服务崩溃问题
    设计模式-行为型-状态模式
    Java/ExecutorService中多线程服务ExecuteService的使用
    Linux:rwx操作对目录的操作效果+rwx的最小权限问题
    VUEX版数字求和案例,附带vuex工作执行顺序图
    【构建并发程序】3-原子变量
    新知识:去掉li前面的项目符号(小圆点)
    基础组件-流量回放(全链路流量回放预研)
  • 原文地址:https://blog.csdn.net/m0_51755720/article/details/127890869