首页 前端知识 HTML5 游戏高级教程(四)

HTML5 游戏高级教程(四)

2024-10-17 10:10:52 前端知识 前端哥 646 620 我要收藏

原文:Pro HTML5 Games

协议:CC BY-NC-SA 4.0

八、添加更多游戏元素

在前一章中,我们开发了一个结合寻路和转向的单元移动框架。我们使用这个框架来实现车辆的移动和部署命令。最后,我们通过在中间绘制周期中插入移动步骤,使我们的单元移动看起来更平滑。

我们现在有一个游戏,玩家可以选择单位并命令他们在地图上移动。

在这一章中,我们将在这段代码的基础上添加更多的游戏元素。我们将从实现一种经济开始,玩家可以通过收割来赚钱,然后把钱花在建造建筑和单位上。

然后,我们将构建一个框架来创建游戏级别内的脚本事件,我们可以使用它来控制游戏故事线。我们还将添加向用户显示消息和通知的功能。然后,我们将使用这些元素来处理一个级别内的任务的完成。

我们开始吧。我们将使用第七章中的代码作为起点。

实现基本经济

我们的游戏将会有一个相当简单的经济系统。玩家开始每个任务时会有一笔初始资金。然后,他们可以通过在油田部署收割机来赚取更多。玩家将能够在侧边栏中看到他们的现金余额。一旦玩家有了足够的钱,他们就可以使用工具条来购买建筑和单位。

我们要做的第一件事是修改游戏,以便在关卡开始时向玩家提供金钱。

设定启动资金

我们将从移除物品数组中的一些额外物品开始,并在 maps.js 中的第一张地图中指定两位玩家的起始现金,如清单 8-1 中的所示。

清单 8-1。 设置关卡的起始现金金额(maps.js)

/* Entities to be added */
"items":[
    {"type":"buildings","name":"base","x":11,"y":14,"team":"blue"},
    {"type":"buildings","name":"starport","x":18,"y":14,"team":"blue"},

    {"type":"vehicles","name":"harvester","x":16,"y":12,"team":"blue","direction":3, "uid":-1},
    {"type":"terrain","name":"oilfield","x":3,"y":5,"action":"hint"},

    {"type":"terrain","name":"bigrocks","x":19,"y":6},
    {"type":"terrain","name":"smallrocks","x":8,"y":3}
],

/* Economy Related*/
"cash":{
    "blue":5000,
    "green":1000
},

我们把所有不必要的项目从项目清单上删除了。我们还添加了一个现金对象,将蓝队的起始现金设置为 5000,绿队的起始现金设置为 1000。

您可能已经注意到,我们已经为收割机指定了一个 UID。我们将在本章后面处理触发器和脚本事件时使用它。

image 注意当我们为一个项目指定 UID 时,我们使用负值,这样我们可以确保 UID 永远不会与自动生成的 UID 冲突,后者总是正值。

接下来,我们需要在 singleplayer 对象的 startCurrentLevel()方法中加载这些现金值,如清单 8-2 所示。

清单 8-2。 开始关卡时加载现金金额(singleplayer.js)

startCurrentLevel:function(){
    // Load all the items for the level
    var level = maps.singleplayer[singleplayer.currentLevel];

    // Don't allow player to enter mission until all assets for the level are loaded
    $("#entermission").attr("disabled", true);

    // Load all the assets for the level
    game.currentMapImage = loader.loadImage(level.mapImage);
    game.currentLevel = level;

    game.offsetX = level.startX * game.gridSize;
    game.offsetY = level.startY * game.gridSize;

    // Load level Requirements
    game.resetArrays();
    for (var type in level.requirements){
           var requirementArray = level.requirements[type];
           for (var i=0; i < requirementArray.length; i++) {
               var name = requirementArray[i];
               if (window[type]){
                   window[type].load(name);
               } else {
                   console.log('Could not load type :',type);
               }
           };
    };

    for (var i = level.items.length - 1; i >= 0; i--){
        var itemDetails = level.items[i];
        game.add(itemDetails);
    };

    // Create a grid that stores all obstructed tiles as 1 and unobstructed as 0
    game.currentMapTerrainGrid = [];
    for (var y=0; y < level.mapGridHeight; y++) {
        game.currentMapTerrainGrid[y] = [];
        for (var x=0; x< level.mapGridWidth; x++) {
           game.currentMapTerrainGrid[y][x] = 0;
        }
    };
    for (var i = level.mapObstructedTerrain.length - 1; i >= 0; i--){
        var obstruction = level.mapObstructedTerrain[i];
        game.currentMapTerrainGrid[obstruction[1]][obstruction[0]] = 1;
    };
    game.currentMapPassableGrid = undefined;

    // Load Starting Cash For Game
    game.cash = $.extend([],level.cash);

    // Enable the enter mission button once all assets are loaded
    if (loader.loaded){
        $("#entermission").removeAttr("disabled");
    } else {
        loader.onload = function(){
            $("#entermission").removeAttr("disabled");
        }
    }

    // Load the mission screen with the current briefing
    $('#missonbriefing').html(level.briefing.replace(/\n/g,'<br><br>'));
    $("#missionscreen").show();
},

此时,游戏应该加载关卡加载时双方玩家的起始现金金额。然而,在我们看到现金价值之前,我们需要实现侧栏。

实现侧栏

我们将在 sidebar.js 的侧边栏对象中实现侧边栏功能,如清单 8-3 所示。

清单 8-3。 创建侧栏对象(sidebar.js)

var sidebar = {
    animate:function(){
        // Display the current cash balance value
        $('#cash').html(game.cash[game.team]);
    },
}

目前,该对象只有 animate()方法,它更新侧栏现金值。我们将从游戏对象的 animationLoop()方法中调用这个方法,如清单 8-4 所示。

清单 8-4。 从 game.animationLoop() (game.js)调用 sidebar.animate()

animationLoop:function(){
    // Animate the Sidebar
    sidebar.animate();

    // Process orders for any item that handles it
    for (var i = game.items.length - 1; i >= 0; i--){
        if(game.items[i].processOrders){
            game.items[i].processOrders();
        }
    };

    // Animate each of the elements within the game
    for (var i = game.items.length - 1; i >= 0; i--){
        game.items[i].animate();
    };

    // Sort game items into a sortedItems array based on their x,y coordinates
    game.sortedItems = $.extend([],game.items);
    game.sortedItems.sort(function(a,b){
        return b.y-a.y + ((b.y==a.y)?(a.x-b.x):0);
    });

    game.lastAnimationTime = (new Date()).getTime();
},

接下来,我们将在 index.html 的部分添加对 sidebar.js 的引用,如清单 8-5 所示。

清单 8-5。 添加对 sidebar.js 的引用(index.html)

<script src="js/sidebar.js" type="text/javascript" charset="utf-8"></script>

如果我们运行代码到目前为止,我们应该在侧边栏区域看到玩家的现金余额,如图图 8-1 所示。

9781430247104_Fig08-01.jpg

图 8-1。侧栏显示现金余额

现在我们有了一个有现金余额的基本侧边栏,我们将为玩家实现一种通过收获获得更多钱的方法。

产生金钱

在前一章中,我们已经实现了部署收割机的能力。为了在收割时开始赚钱,我们将修改部署动画状态,并在 buildings.js 中的默认 animate()方法中实现一个新的收割动画状态,如清单 8-6 中的所示。

清单 8-6。 在 animate() (buildings.js)内部实现新的收割动画状态

case "deploy":
    this.imageList = this.spriteArray["deploy"];
    this.imageOffset = this.imageList.offset + this.animationIndex;
    this.animationIndex++;
    // Once deploying is complete, go to harvest now
    if (this.animationIndex>=this.imageList.count){
        this.animationIndex = 0;
        this.action = "harvest";
    }
    break;
case "harvest":
    this.imageList = this.spriteArray[this.lifeCode];
    this.imageOffset = this.imageList.offset + this.animationIndex;
    this.animationIndex++;
    if (this.animationIndex>=this.imageList.count){
        this.animationIndex = 0;
        if (this.lifeCode == "healthy"){
            // Harvesters mine 2 credits of cash per animation cycle
            game.cash[this.team] += 2;
        }
    }
    break;

收获箱与立箱相似。然而,每当动画运行一个完整的周期,我们会在玩家的现金余额中增加两个信用点。只有在矿车建筑没有损坏的情况下,我们才会这么做。

我们还修改了 deploy 状态,使其转入 harvest 状态,而不是 stand 状态。这样一旦部署了收割机,就会自动开始赚钱。

如果我们开始游戏,将矿车部署到油田,应该会看到现金余额慢慢增加,如图图 8-2 所示。

9781430247104_Fig08-02.jpg

图 8-2。部署收割机慢慢挣钱

我们现在有一个基本的游戏经济设置。我们准备实现购买建筑物和单位。

购买建筑物和单元

在我们的游戏中,基地建筑被用来建造建筑,星港被用来建造车辆和飞机。玩家可以通过选择他们想要建造的建筑来购买物品,然后点击工具条上相应的购买按钮。

我们首先将这些购买按钮添加到我们的侧边栏。

添加侧边栏按钮

我们首先将按钮的 HTML 标记添加到 index.html 内部的 gameinterfacescreen div 中,如清单 8-7 所示。

清单 8-7。 添加侧边栏购买按钮到游戏界面屏幕(index.html)

<div id="gameinterfacescreen" class="gamelayer">
    <div id="gamemessages"></div>
    <div id="callerpicture"></div>
    <div id="cash"></div>
    <div id="sidebarbuttons">
        <input type="button" id="starportbutton" title = "Starport">
        <input type="button" id="turretbutton" title = "Turret">
        <input type="button" id="placeholder1" disabled>

        <input type="button" id="scouttankbutton" title = "Scout Tank">
        <input type="button" id="heavytankbutton" title = "Heavy Tank">
        <input type="button" id="harvesterbutton" title = "Harvester">

        <input type="button" id="chopperbutton" title = "Copter">
        <input type="button" id="wraithbutton" title = "Wraith">
        <input type="button" id="placeholder2" disabled>
    </div>
    <canvas id="gamebackgroundcanvas" height="400" width="480"></canvas>
    <canvas id="gameforegroundcanvas" height="400" width="480"></canvas>
</div>

接下来我们将为这些按钮添加合适的 CSS 样式到 styles.css 中,如清单 8-8 所示。

清单 8-8。 侧边栏按钮的 CSS 样式(styles.css)

/* Sidebar Buttons */
#gameinterfacescreen #sidebarbuttons {
    position:absolute;
    left:500px;
    top:305px;
    width:152px;
    height:148px;
    overflow:none;
}

#gameinterfacescreen #sidebarbuttons input[type="button"] {
    width:43px;
    height:35px;
    border-width:0px;
    padding:0px;
    background-image: url(img/buttons.png);
}

/* Grayed out state for buttons*/
#starportbutton:active, #starportbutton:disabled {
    background-position: -2px -305px;
}
#placeholder1:active, #placeholder1:disabled {
    background-position: -52px -305px;
}
#turretbutton:active, #turretbutton:disabled {
    background-position: -100px -305px;
}
#scouttankbutton:active, #scouttankbutton:disabled {
    background-position: -2px -346px;
}
#heavytankbutton:active, #heavytankbutton:disabled {
    background-position: -52px -346px;
}
#harvesterbutton:active, #harvesterbutton:disabled {
    background-position: -102px -346px;
}
#chopperbutton:active, #chopperbutton:disabled {
    background-position: -2px -387px;
}
#placeholder2:active, #placeholder2:disabled {
    background-position: -52px -387px;
}
#wraithbutton:active, #wraithbutton:disabled {
    background-position: -102px -387px;
}

/* Regular state for buttons*/
#starportbutton {
    background-position: -167px -305px;
}
#placeholder1 {
    background-position: -216px -305px;
}
#turretbutton {
    background-position: -264px -305px;
}
#scouttankbutton {
    background-position: -167px -346px;
}
#heavytankbutton {
    background-position: -216px -346px;
}
#harvesterbutton {
    background-position: -264px -346px;
}
#chopperbutton {
    background-position: -167px -387px;
}
#placeholder2 {
    background-position: -216px -387px;
}
#wraithbutton {
    background-position: -264px -387px;
}

HTML 标记将按钮添加到侧边栏,而 CSS 样式使用 buttons.png 文件定义这些按钮的图像。

如果我们在浏览器中运行游戏,我们应该会在工具条中看到购买按钮,如图图 8-3 所示。

9781430247104_Fig08-03.jpg

图 8-3。侧边栏中的购买按钮

此时,所有的按钮看起来都是激活的;但是,单击按钮不会做任何事情。根据是否允许玩家构建物品,需要启用或禁用按钮。

启用和禁用侧栏按钮

我们要做的下一件事是确保侧边栏按钮只有在选择了合适的建筑并且玩家有足够的钱来建造物品时才被激活。我们将通过向 sidebar.js 添加一个 enableSidebarButtons()方法并从 animate()方法内部调用它来实现,如清单 8-9 所示。

清单 8-9。 启用和禁用侧边栏按钮(sidebar.js)

var sidebar = {
    enableSidebarButtons:function(){
        // Buttons only enabled when appropriate building is selected
        $("#gameinterfacescreen #sidebarbuttons input[type='button'] ").attr("disabled", true);

        // If no building selected, then no point checking below
        if (game.selectedItems.length==0){
            return;
        }
        var baseSelected = false;
        var starportSelected = false;
        // Check if base or starport is selected
        for (var i = game.selectedItems.length - 1; i >= 0; i--){
            var item = game.selectedItems[i];
            //  Check If player selected a healthy,inactive building (damaged buildings can't produce)
if (item.type == "buildings" && item.team == game.team && item.lifeCode == "healthy" && item.action=="stand"){
                if(item.name == "base"){
                    baseSelected = true;
                } else if (item.name == "starport"){
                    starportSelected = true;
                }
            }
        };

        var cashBalance = game.cash[game.team];
        /* Enable building buttons if base is selected,building has been loaded in requirements, not in deploy building mode and player has enough money*/
        if (baseSelected && !game.deployBuilding){
            if(game.currentLevel.requirements.buildings.indexOf('starport')>-1 && cashBalance>=buildings.list["starport"].cost){
                $("#starportbutton").removeAttr("disabled");
            }
                              if(game.currentLevel.requirements.buildings.indexOf('ground-turret')>-1 && cashBalance>=buildings.list["ground-turret"].cost){
                $("#turretbutton").removeAttr("disabled");
            }
        }

        /* Enable unit buttons if starport is selected, unit has been loaded in requirements, and player has enough money*/
        if (starportSelected){
                              if(game.currentLevel.requirements.vehicles.indexOf('scout-tank')>-1 && cashBalance>=vehicles.list["scout-tank"].cost){
                $("#scouttankbutton").removeAttr("disabled");
            }
if(game.currentLevel.requirements.vehicles.indexOf('heavy-tank')>-1 && cashBalance>=vehicles.list["heavy-tank"].cost){
                $("#heavytankbutton").removeAttr("disabled");
            }
            if(game.currentLevel.requirements.vehicles.indexOf('harvester')>-1 && cashBalance>=vehicles.list["harvester"].cost){
                $("#harvesterbutton").removeAttr("disabled");
            }
            if(game.currentLevel.requirements.aircraft.indexOf('chopper')>-1 && cashBalance>=aircraft.list["chopper"].cost){
                $("#chopperbutton").removeAttr("disabled");
            }
            if(game.currentLevel.requirements.aircraft.indexOf('wraith')>-1 && cashBalance>=aircraft.list["wraith"].cost){
                $("#wraithbutton").removeAttr("disabled");
            }
        }
    },
    animate:function(){
        // Display the current cash balance value
        $('#cash').html(game.cash[game.team]);

        //  Enable or disable buttons as appropriate
        this.enableSidebarButtons();
    },
}

在 enableSidebarButton()方法中,我们首先默认禁用所有按钮。然后,我们检查是否选择了有效的 base 或 starport。一个有效的基地或星港属于玩家,是健康的,并且当前处于站立模式,这意味着它当前没有建造任何其他东西。

如果基地已经被选中,建筑类型已经载入关卡要求,并且玩家有足够的现金购买建筑,我们就会启用建筑按钮。如果选择了一个有效的星港,我们对车辆和飞机做同样的事情。

如果我们现在运行游戏,一旦我们选择了一个基地或星港,侧边栏按钮就会被激活,如图 8-4 所示。

9781430247104_Fig08-04.jpg

图 8-4。侧边栏建筑建造按钮通过选择基础启用

如图所示,建筑物按钮已启用,而车辆和飞机按钮被禁用,因为选择了基础。我们同样可以通过选择星港来激活车辆和飞机建造按钮。

现在是时候在星港建造车辆和飞机了。

在星港建造车辆和飞机

我们要做的第一件事是通过添加清单 8-10 中的代码来修改侧边栏对象以处理按钮的点击事件。

清单 8-10。 设置侧栏按钮的点击事件(Sidebar . js)

init:function(){
    $("#scouttankbutton").click(function(){
        sidebar.constructAtStarport({type:"vehicles","name":"scout-tank"});
    });
    $("#heavytankbutton").click(function(){
        sidebar.constructAtStarport({type:"vehicles","name":"heavy-tank"});
    });
    $("#harvesterbutton").click(function(){
        sidebar.constructAtStarport({type:"vehicles","name":"harvester"});
    });
    $("#chopperbutton").click(function(){
        sidebar.constructAtStarport({type:"aircraft","name":"chopper"});
    });
    $("#wraithbutton").click(function(){
        sidebar.constructAtStarport({type:"aircraft","name":"wraith"});
    });
},
constructAtStarport:function(unitDetails){
    var starport;
    // Find the first eligible starport among selected items
    for (var i = game.selectedItems.length - 1; i >= 0; i--){
        var item = game.selectedItems[i];
        if (item.type == "buildings" && item.name == "starport"
            && item.team == game.team && item.lifeCode == "healthy" && item.action=="stand"){
            starport = item;
            break;
        }
    };
    if (starport){
        game.sendCommand([starport.uid],{type:"construct-unit",details:unitDetails});
    }
},

我们首先声明一个 init()方法,为每个车辆和飞机按钮设置 click 事件,以使用适当的单元细节调用 constructAtStarport()方法。

在 constructAtStarport()方法中,我们获得了所选项目中第一个合格的 Starport。然后,我们使用 game.sendCommand()方法向 starport 发送一个构造单元命令,其中包含要构造的单元的详细信息。

接下来,我们将在游戏初始化时从 game.init()方法内部调用 sidebar.init()方法,如清单 8-11 所示。

清单 8-11。 从 game.init() (game.js)内部初始化侧边栏

 // Start preloading assets
init: function(){
    loader.init();
    mouse.init();
    sidebar.init();

    $('.gamelayer').hide();
    $('#gamestartscreen').show();

    game.backgroundCanvas = document.getElementById('gamebackgroundcanvas');
    game.backgroundContext = game.backgroundCanvas.getContext('2d');

    game.foregroundCanvas = document.getElementById('gameforegroundcanvas');
    game.foregroundContext = game.foregroundCanvas.getContext('2d');

    game.canvasWidth = game.backgroundCanvas.width;
    game.canvasHeight = game.backgroundCanvas.height;
},

接下来,我们将为 starport 建筑创建一个 processOrder()方法,它实现了构造单元订单。我们将在 starport 定义中添加这个方法,如清单 8-12 所示。

清单 8-12。 在 Starport 定义(buildings.js)内实现 processOrder()

"starport":{
    name:"starport",
    pixelWidth:40,
    pixelHeight:60,
    baseWidth:40,
    baseHeight:55,
    pixelOffsetX:1,
    pixelOffsetY:5,
    buildableGrid:[
        [1,1],
        [1,1],
        [1,1]
    ],
    passableGrid:[
        [1,1],
        [0,0],
        [0,0]
    ],
    sight:3,
    cost:2000,
      hitPoints:300,
    spriteImages:[
        {name:"teleport",count:9},
        {name:"closing",count:18},
        {name:"healthy",count:4},
        {name:"damaged",count:1},
    ],
    processOrders:function(){
        switch (this.orders.type){
            case "construct-unit":
                if(this.lifeCode != "healthy"){
                    return;
                }
                // First make sure there is no unit standing on top of the building
                var unitOnTop = false;
                for (var i = game.items.length - 1; i >= 0; i--){
                    var item = game.items[i];
                    if (item.type == "vehicles" || item.type == "aircraft"){
                         if (item.x > this.x && item.x < this.x+2 && item.y> this.y && item.y<this.y+3){
                            unitOnTop = true;
                            break;
                        }
                    }
                };

                var cost = window[this.orders.details.type].list[this.orders.details.name].cost;
                if (unitOnTop){
                    if (this.team == game.team){
                        game.showMessage("system","Warning! Cannot teleport unit while landing bay is occupied.");
                    }
                } else if(game.cash[this.team]<cost){
                    if (this.team == game.team){
                        game.showMessage("system","Warning! Insufficient Funds. Need "+cost+ " credits.");
                    }
                } else {
                    this.action="open";
                    this.animationIndex = 0;
                    // Position new unit above center of starport
                    var itemDetails = this.orders.details;
                    itemDetails.x = this.x+0.5*this.pixelWidth/game.gridSize;
                    itemDetails.y = this.y+0.5*this.pixelHeight/game.gridSize;
                    // Teleport in unit and subtract the cost from player cash
                    itemDetails.action="teleport";
                    itemDetails.team = this.team;
                    game.cash[this.team] -= cost;
                    this.constructUnit = $.extend(true,[],itemDetails);

                }
                this.orders = {type:"stand"};
                break;
        }
    }
},

我们首先检查是否有任何单位已经被放置在星港上方,如果是,我们使用 game.showMessage()方法通知玩家当登陆舱被占用时不能传送单位。接下来,我们检查我们是否有足够的资金,如果没有,通知用户。

最后,我们实现了单元的实际购买。我们首先将建筑物的动画动作设置为打开。然后,我们为该项设置位置、动作和团队属性。我们将新单位的详细信息保存在 constructUnit 变量中,最后从玩家的现金余额中减去该物品的费用。

你可能已经注意到我们为新建造的单位设置了一个传送动作。我们需要在车辆和飞机上实现这一点。

接下来,我们将修改 buildings 对象的 animate()方法中的开放动画状态,以将该单位添加到游戏中,如清单 8-13 所示。

清单 8-13。 星港开启后添加单位(buildings.js)

case "open":
    this.imageList = this.spriteArray["closing"];
    // Opening is just the closing sprites running backwards
    this.imageOffset = this.imageList.offset + this.imageList.count - this.animationIndex;
    this.animationIndex++;
    // Once opening is complete, go back to close
    if (this.animationIndex>=this.imageList.count){
        this.animationIndex = 0;
        this.action = "close";
        // If constructUnit has been set, add the new unit to the game
        if(this.constructUnit){
            game.add(this.constructUnit);
            this.constructUnit = undefined;
        }
    }
     break;

一旦打开动画完成,我们检查是否设置了 constructUnit 属性,如果设置了,我们在取消设置变量之前将单位添加到游戏中。

接下来我们将在游戏对象中实现一个 showMessage()方法,如清单 8-14 所示。

清单 8-14。 游戏对象的 showMessage()方法

// Functions for communicating with player
characters: {
    "system":{
        "name":"System",
        "image":"img/system.png"
    }
},
showMessage:function(from,message){
    var character = game.characters[from];
    if (character){
        from = character.name;
        if (character.image){
            $('#callerpicture').html('<img src="'+character.image+'"/>');
            // hide the profile picture after six seconds
            setTimeout(function(){
                $('#callerpicture').html("");
            },6000)
        }
    }
    // Append message to messages pane and scroll to the bottom
    var existingMessage = $('#gamemessages').html();
    var newMessage = existingMessage+'<span>'+from+': </span>'+message+'<br>';
    $('#gamemessages').html(newMessage);
    $('#gamemessages').animate({scrollTop:$('#gamemessages').prop('scrollHeight')});
}

我们首先定义一个 characters 对象,它包含系统角色的名称和图像。在 showMessage()方法中,我们检查 from 参数是否有字符图像,如果有,则显示该图像四秒钟。接下来,我们将消息追加到 gamemessages div,并滚动到 div 的底部。

无论何时调用 showMessage()方法,都会在消息窗口显示消息,在侧边栏显示图片,如图图 8-5 所示。

9781430247104_Fig08-05.jpg

图 8-5。使用 showMessage()显示系统警告

当我们推进游戏故事线时,我们可以使用这种机制来显示来自各种游戏角色的玩家对话。这将使单人战役更受剧情驱动,并使游戏更具吸引力。

最后,我们将修改车辆和飞机物体来实现新的传送动作。

我们将从在 vehicles 对象的 animate()方法中的 stand 动作的正下方添加一个传送动作的案例开始,如清单 8-15 所示。

清单 8-15。 在 animate() (vehicles.js)内添加瞬移动作的案例

case "teleport":
    var direction = wrapDirection(Math.round(this.direction),this.directions);
    this.imageList = this.spriteArray["stand-"+direction];
    this.imageOffset = this.imageList.offset + this.animationIndex;
    this.animationIndex++;

    if (this.animationIndex>=this.imageList.count){
        this.animationIndex = 0;
    }
    if (!this.brightness){
        this.brightness = 1;
    }
    this.brightness -= 0.05;
    if(this.brightness <= 0){
        this.brightness = undefined;
        this.action = "stand";
    }
    break;

我们首先设置 imageOffset 和 animationIndex,就像我们为默认的 stand 动作所做的那样。然后,我们将一个亮度变量设置为 1,并逐渐将其减少到 0,此时我们将动作状态切换回 stand。

接下来,我们将修改 vehicles 对象的默认 draw()方法来使用 brightness 属性,如清单 8-16 所示。

清单 8-16。 修改 draw()方法处理瞬移亮度(vehicles.js)

draw:function(){
    var x = (this.x*game.gridSize)-game.offsetX-this.pixelOffsetX + this.lastMovementX*game.drawingInterpolationFactor*game.gridSize;
    var y = (this.y*game.gridSize)-game.offsetY-this.pixelOffsetY + this.lastMovementY*game.drawingInterpolationFactor*game.gridSize;
    this.drawingX = x;
    this.drawingY = y;

    if (this.selected){
        this.drawSelection();
        this.drawLifeBar();
    }

    var colorIndex = (this.team == "blue")?0:1;
    var colorOffset = colorIndex*this.pixelHeight;

    game.foregroundContext.drawImage(this.spriteSheet, this.imageOffset*this.pixelWidth,colorOffset,
           this.pixelWidth,this.pixelHeight,x,y,this.pixelWidth,this.pixelHeight);

    // Draw glow while teleporting in
    if(this.brightness){
        game.foregroundContext.beginPath();
        game.foregroundContext.arc(x+ this.pixelOffsetX, y+this.pixelOffsetY, this.radius, 0 , Math.PI*2,false);
        game.foregroundContext.fillStyle = 'rgba(255,255,255,'+this.brightness+')';
        game.foregroundContext.fill();
    }
}

在新添加的代码中,我们检查车辆是否设置了亮度属性,如果是,我们在车辆顶部绘制一个填充的白色圆圈,并根据亮度填充 alpha 值。由于 brightness 属性的值从 1 下降到 0,圆将逐渐从亮白色变为完全透明。

接下来,我们将在飞行器对象的 animate()方法中的 fly 动作下面添加一个传送动作的例子,如清单 8-17 所示。

清单 8-17。 在 animate() (aircraft.js)内部添加瞬移动作案例

case "teleport":
    var direction = wrapDirection(Math.round(this.direction),this.directions);
    this.imageList = this.spriteArray["fly-"+direction];
    this.imageOffset = this.imageList.offset + this.animationIndex;
    this.animationIndex++;

    if (this.animationIndex>=this.imageList.count){
        this.animationIndex = 0;
    }
    if (!this.brightness){
        this.brightness = 1;
    }
    this.brightness -= 0.05;
    if(this.brightness <= 0){
        this.brightness = undefined;
        this.action = "fly";
    }
    break;

与我们对车辆所做的类似,我们设置一个亮度属性,逐渐将其降低到 0,然后将动作状态设置为飞行。

最后,我们将修改飞机对象的默认 draw()方法来使用亮度属性,就像我们对车辆所做的一样,如清单 8-18 所示。

清单 8-18。 修改 draw()方法处理瞬移亮度(aircraft.js)

draw:function(){
    var x = (this.x*game.gridSize)-game.offsetX-this.pixelOffsetX + this.lastMovementX*game.drawingInterpolationFactor*game.gridSize;
    var y = (this.y*game.gridSize)-game.offsetY-this.pixelOffsetY-this.pixelShadowHeight + this.lastMovementY*game.drawingInterpolationFactor*game.gridSize;
    this.drawingX = x;
    this.drawingY = y;
    if (this.selected){
        this.drawSelection();
        this.drawLifeBar();
    }
    var colorIndex = (this.team == "blue")?0:1;
    var colorOffset = colorIndex*this.pixelHeight;
    var shadowOffset = this.pixelHeight*2; // The aircraft shadow is on the second row of the sprite sheet

    game.foregroundContext.drawImage(this.spriteSheet, this.imageOffset*this.pixelWidth,colorOffset, this.pixelWidth, this.pixelHeight, x,y,this.pixelWidth,this.pixelHeight);
    game.foregroundContext.drawImage(this.spriteSheet, this.imageOffset*this.pixelWidth,shadowOffset,this.pixelWidth, this.pixelHeight, x, y+this.pixelShadowHeight, this.pixelWidth,this.pixelHeight);

    // Draw glow while teleporting in
    if(this.brightness){
        game.foregroundContext.beginPath();
        game.foregroundContext.arc(x+ this.pixelOffsetX,y+this.pixelOffsetY,this.radius,0,Math.PI*2,false);
        game.foregroundContext.fillStyle = 'rgba(255,255,255,'+this.brightness+')';
        game.foregroundContext.fill();
    }
}

如果你在浏览器中运行游戏,你现在应该能够选择星港并建造一辆车或飞机,如图 8-6 所示。

9781430247104_Fig08-06.jpg

图 8-6。飞行器瞬移到星际港口

飞机传送到星港的正上方,在一个白色发光的圆圈内。你会注意到,当飞机被传送进来时,侧边栏按钮被禁用。此外,现金余额因飞机成本而减少。当玩家买不起一个单位时,它的按钮会自动失效。当星港有另一个单位在它上面盘旋时,试图建造一个单位将会导致如图 8-5 所示的系统警告。

现在我们已经完成了建造车辆和飞机,是时候在基地建造建筑了。

在基地建造建筑

我们将从在侧边栏对象的 init()方法中为两个建筑构造按钮设置 click 事件开始,如清单 8-19 中的所示。

清单 8-19。 设置建筑按钮的点击事件(sidebar.js)

init:function(){
    // Initialize unit construction buttons
    $("#scouttankbutton").click(function(){
        sidebar.constructAtStarport({type:"vehicles","name":"scout-tank"});
    });
    $("#heavytankbutton").click(function(){
        sidebar.constructAtStarport({type:"vehicles","name":"heavy-tank"});
    });
    $("#harvesterbutton").click(function(){
        sidebar.constructAtStarport({type:"vehicles","name":"harvester"});
    });
    $("#chopperbutton").click(function(){
        sidebar.constructAtStarport({type:"aircraft","name":"chopper"});
    });
    $("#wraithbutton").click(function(){
        sidebar.constructAtStarport({type:"aircraft","name":"wraith"});
    });

    //Initialize building construction buttons

    $("#starportbutton").click(function(){
        game.deployBuilding = "starport";
    });
    $("#turretbutton").click(function(){
        game.deployBuilding = "ground-turret";
    });
},

当单击两个 building-construction 按钮中的任何一个时,我们将 sidebar.deployBuilding 属性设置为要构建的建筑物的名称。

接下来,我们将修改侧边栏 animate()方法来处理建筑物的部署,如清单 8-20 所示。

清单 8-20。 修改 animate()方法处理建筑部署(sidebar.js)

animate:function(){
    // Display the current cash balance value
    $('#cash').html(game.cash[game.team]);
    //  Enable or disable buttons as appropriate
    this.enableSidebarButtons();

    if (game.deployBuilding){
        // Create the buildable grid to see where building can be placed
        game.rebuildBuildableGrid();
        // Compare with buildable grid to see where we need to place the building
        var placementGrid = buildings.list[game.deployBuilding].buildableGrid;
        game.placementGrid = $.extend(true,[],placementGrid);
        game.canDeployBuilding = true;
        for (var i = game.placementGrid.length - 1; i >= 0; i--){
            for (var j = game.placementGrid[i].length - 1; j >= 0; j--){
                if(game.placementGrid[i][j] &&
                    (mouse.gridY+i>= game.currentLevel.mapGridHeight || mouse.gridX+j>=game.currentLevel.mapGridWidth || game.currentMapBuildableGrid[mouse.gridY+i][mouse.gridX+j]==1)){
                    game.canDeployBuilding = false;
                    game.placementGrid[i][j] = 0;
                }
            };
        };
    }
},

如果已经设置了 game.deployBuilding 变量,我们调用 game.rebuildBuildableGrid()方法创建 game.currentMapBuildableGrid 数组,然后使用正在部署的建筑物的 BuildableGrid 属性设置 game.placementGrid 变量。

然后,我们遍历放置网格,检查是否有可能在当前鼠标位置部署建筑。如果要在其上放置建筑物的任何方块在地图边界之外,或者在 currentMapBuildableGrid 数组中被标记为不可建造,我们将 placementGrid 数组上相应的方块标记为不可建造,并将 canDeployBuilding 标志设置为 false。

接下来我们将在游戏对象内部实现 rebuildBuildableGrid()方法,如清单 8-21 所示。

清单 8-21。 在 rebuildBuildableGrid()方法中创建 buildableGrid(game . js)

rebuildBuildableGrid:function(){
    game.currentMapBuildableGrid = $.extend(true,[],game.currentMapTerrainGrid);
    for (var i = game.items.length - 1; i >= 0; i--){
        var item = game.items[i];
        if(item.type == "buildings" || item.type == "terrain"){
            for (var y = item.buildableGrid.length - 1; y >= 0; y--){
                for (var x = item.buildableGrid[y].length - 1; x >= 0; x--){
                    if(item.buildableGrid[y][x]){
                        game.currentMapBuildableGrid[item.y+y][item.x+x] = 1;
                    }
                };
            };
        } else if (item.type == "vehicles"){
            // Mark all squares under or near the vehicle as unbuildable
            var radius = item.radius/game.gridSize;
            var x1 = Math.max(Math.floor(item.x - radius),0);
            var x2 = Math.min(Math.floor(item.x + radius),game.currentLevel.mapGridWidth-1);
            var y1 = Math.max(Math.floor(item.y - radius),0);
            var y2 = Math.min(Math.floor(item.y + radius),game.currentLevel.mapGridHeight-1);
            for (var x=x1; x <= x2; x++) {
                for (var y=y1; y <= y2; y++) {
                    game.currentMapBuildableGrid[y][x] = 1;
                };
            };
        }
    };
},

我们首先将 currentMapBuildableGrid 初始化为 currentMapTerrainGrid。然后,我们将建筑或地形实体下的所有方块标记为不可建筑,就像我们在创建可通行数组时所做的那样。最后,我们将车辆旁边的所有方格标记为不可建造。

接下来我们将修改鼠标对象的 draw()方法来标记建筑将要部署的网格位置,如清单 8-22 所示。

清单 8-22。 绘制鼠标光标下的建筑部署网格(Mouse . js)

draw:function(){
    if(this.dragSelect){
        var x = Math.min(this.gameX,this.dragX);
        var y = Math.min(this.gameY,this.dragY);
        var width = Math.abs(this.gameX-this.dragX)
        var height = Math.abs(this.gameY-this.dragY)
        game.foregroundContext.strokeStyle = 'white';
        game.foregroundContext.strokeRect(x-game.offsetX,y-    game.offsetY, width, height);
    }
    if (game.deployBuilding && game.placementGrid){
        var buildingType = buildings.list[game.deployBuilding];
        var x = (this.gridX*game.gridSize)-game.offsetX;
        var y = (this.gridY*game.gridSize)-game.offsetY;
        for (var i = game.placementGrid.length - 1; i >= 0; i--){
            for (var j = game.placementGrid[i].length - 1; j >= 0; j--){
                if(game.placementGrid[i][j]){
                    game.foregroundContext.fillStyle = "rgba(0,0,255,0.3)";
                } else {
                    game.foregroundContext.fillStyle = "rgba(255,0,0,0.3)";
                }
                game.foregroundContext.fillRect(x+j*game.gridSize, y+i*game.gridSize, game.gridSize, game.gridSize);
            };
        };
    }
},

我们首先检查是否已经设置了 deployBuilding 和 placementGrid 变量,如果已经设置了,我们根据是否可以在该网格位置放置建筑物来绘制蓝色或红色方块。

如果你现在运行游戏,选择主基地,并尝试创建一个建筑,你应该会看到建筑在鼠标位置展开网格,如图图 8-7 所示。

9781430247104_Fig08-07.jpg

图 8-7。构建部署网格,用红色标记不可构建的方块

现在我们可以启动建筑物部署模式,我们将通过单击鼠标左键来放置建筑物,或者通过单击鼠标右键来取消模式。我们将从修改鼠标对象的 click()方法开始,如清单 8-23 所示。

清单 8-23。 修改 mouse.click() 完成或取消部署模式(mouse.js)

click:function(ev,rightClick){
    // Player clicked inside the canvas

    var clickedItem = this.itemUnderMouse();
    var shiftPressed = ev.shiftKey;

    if (!rightClick){ // Player left clicked
        // If the game is in deployBuilding mode, left clicking will deploy the building
        if (game.deployBuilding){
            if(game.canDeployBuilding){
                sidebar.finishDeployingBuilding();
            } else {
                game.showMessage("system","Warning! Cannot deploy building here.");
            }

            return;
        }
        if (clickedItem){
            // Pressing shift adds to existing selection. If shift is not pressed, clear existing selection
            if(!shiftPressed){
                game.clearSelection();
            }
            game.selectItem(clickedItem,shiftPressed);
        }
    } else { // Player right clicked
        // If the game is in deployBuilding mode, right clicking will cancel deployBuilding mode
        if (game.deployBuilding){
            sidebar.cancelDeployingBuilding();
            return;
        }
        // Handle actions like attacking and movement of selected units
        var uids = [];
        if (clickedItem){ // Player right clicked on something... Specific action
            if (clickedItem.type != "terrain"){
                if (clickedItem.team != game.team){ // Player right clicked on an enemy item
                    for (var i = game.selectedItems.length - 1; i >= 0; i--){
                        var item = game.selectedItems[i];
                        // if selected item is from players team and can attack
                        if(item.team == game.team && item.canAttack){
                            uids.push(item.uid);
                        }
                    };
                    if (uids.length>0){
                        game.sendCommand(uids,{type:"attack",toUid:clickedItem.uid});
                    }
                } else  { // Player right clicked on a friendly item
                    for (var i = game.selectedItems.length - 1; i >= 0; i--){
                        var item = game.selectedItems[i];
                        if(item.team == game.team && (item.type == "vehicles" || item.type == "aircraft")){
                            uids.push(item.uid);
                        }
                    };
                    if (uids.length>0){
                        game.sendCommand(uids,{type:"guard",toUid:clickedItem.uid});
                    }
                }
            } else if (clickedItem.name == "oilfield"){
                // Oilfield means harvesters go and deploy there
                for (var i = game.selectedItems.length - 1; i >= 0; i--){
                    var item = game.selectedItems[i];
                    // pick the first selected harvester since only one can deploy at a time
                    if(item.team == game.team && (item.type == "vehicles" && item.name == "harvester")){
                        uids.push(item.uid);
                        break;
                    }
                };
                if (uids.length>0){
                    game.sendCommand(uids,{type:"deploy",toUid:clickedItem.uid});
                }
            }
        } else { // Just try to move there
            // Get all UIDs that can be commanded to move
            for (var i = game.selectedItems.length - 1; i >= 0; i--){
                var item = game.selectedItems[i];
                if(item.team == game.team && (item.type == "vehicles" || item.type == "aircraft")){
                    uids.push(item.uid);
                }
            };
            if (uids.length>0){
                game.sendCommand(uids,{type:"move",  to:{x:mouse.gameX/game.gridSize, y:mouse.gameY/game.gridSize}});
            }
        }
    }
},

如果玩家在部署模式下单击鼠标左键,我们检查 canDeployBuilding 变量并调用 sidebar . finishdeployingbuilding(),如果我们可以部署建筑物,我们使用 game.showMessage()显示警告消息。

如果播放器在部署模式下右键单击,我们调用 sidebar . canceldeployingbuilding()方法。

接下来我们将在侧边栏对象中实现这两个新方法,finishDeployBuilding() 和 cancelDeployBuilding() ,如清单 8-24 所示。

清单 8-24。 完成部署构建()和取消部署构建()(sidebar.js)

cancelDeployingBuilding:function(){
    game.deployBuilding = undefined;
},
finishDeployingBuilding:function(){
    var buildingName= game.deployBuilding;
    var base;
    for (var i = game.selectedItems.length - 1; i >= 0; i--){
        var item = game.selectedItems[i];
        if (item.type == "buildings" && item.name == "base" && item.team == game.team && item.lifeCode == "healthy" && item.action=="stand"){
            base = item;
            break;
        }
    };

    if (base){
        var buildingDetails = {type:"buildings",name:buildingName,x:mouse.gridX,y:mouse.gridY};
        game.sendCommand([base.uid],{type:"construct-building",details:buildingDetails});
    }

    // Clear deployBuilding flag
    game.deployBuilding = undefined;
}

cancelDeployingBuilding()方法只是清除 deployBuilding 变量。finishDeployingBuilding()方法首先选择基地,然后使用 game.sendCommand()方法向其发送构建命令。

接下来,我们将为实现构造-构建顺序的基础构建创建一个 processOrder()方法。我们将在基本定义中添加这个方法,如清单 8-25 所示。

清单 8-25。 实现基定义(buildings.js)内的 processOrder()

processOrders:function(){
    switch (this.orders.type){
        case "construct-building":
            this.action="construct";
            this.animationIndex = 0;
            var itemDetails = this.orders.details;
            // Teleport in building and subtract the cost from player cash
            itemDetails.team = this.team;
            itemDetails.action = "teleport";
            var item = game.add(itemDetails);
            game.cash[this.team] -= item.cost;
            this.orders = {type:"stand"};
            break;
    }
}

我们首先将基本实体的动作状态设置为构造。接下来,我们把这个建筑添加到游戏中,它的动作状态是传送。最后,我们从现金余额中减去建筑成本,并将基本实体的 orders 属性设置回 stand。

如果你现在运行游戏,并试图通过左键点击地图上的一个有效位置来部署建筑,建筑应该会被传送到那个位置,如图 8-8 所示。

9781430247104_Fig08-08.jpg

图 8-8。部署好的建筑被传送进来

你会注意到现金余额因建筑成本而减少。当玩家买不起建筑时,它的按钮会自动失效。此外,如果您尝试在无效的位置部署建筑物,您将会看到一条系统警告消息,告诉您不能在该位置部署建筑物。

我们现在可以在游戏中建造单位和建筑了。我们将在本章中实现的最后一件事是根据触发的事件结束级别。

结束一关

每当玩家成功完成一个关卡的目标,我们会显示一个消息框通知他们,然后载入下一个关卡。如果玩家任务失败,我们会给玩家重新玩当前关卡或者离开单人战役的选择。

我们将通过在游戏中实现一个触发事件系统来检查成功和失败的标准。在后面的章节中,我们将使用相同的事件系统来编写基于故事的事件。

我们要做的第一件事是实现一个消息对话框。

实现消息对话框

消息框将是一个模式对话框,只有一个确定按钮或同时有确定和取消按钮。

我们首先将消息框屏幕的 HTML 标记添加到 index.html 的主体,如清单 8-26 所示。

清单 8-26。 在正文标签内为消息框添加 HTML 标记(index.html)

<body>
    <div id="gamecontainer">
        <div id="gamestartscreen" class="gamelayer">
            <span id="singleplayer" onclick = "singleplayer.start();">Campaign</span><br>
            <span id="multiplayer" onclick = "multiplayer.start();">Multiplayer</span><br>
        </div>
        <div id="missionscreen" class="gamelayer">
            <input type="button" id="entermission" onclick = "singleplayer.play();">
            <input type="button" id="exitmission" onclick = "singleplayer.exit();">
            <div id="missonbriefing">Welcome to your first mission.
            </div>
        </div>
        <div id="gameinterfacescreen" class="gamelayer">
            <div id="gamemessages"></div>
            <div id="callerpicture"></div>
            <div id="cash"></div>
            <div id="sidebarbuttons">
                <input type="button" id="starportbutton" title = "Starport">
                <input type="button" id="turretbutton" title = "Turret">
                <input type="button" id="placeholder1" disabled>

                <input type="button" id="scouttankbutton" title = "Scout Tank">
                <input type="button" id="heavytankbutton" title = "Heavy Tank">
                <input type="button" id="harvesterbutton" title = "Harvester">

                <input type="button" id="chopperbutton" title = "Copter">
                <input type="button" id="wraithbutton" title = "Wraith">
                <input type="button" id="placeholder2" disabled>
            </div>
            <canvas id="gamebackgroundcanvas" height="400" width="480"></canvas>
            <canvas id="gameforegroundcanvas" height="400" width="480"></canvas>
        </div>
        <div id="messageboxscreen" class="gamelayer">
            <div id="messagebox">
                <span id="messageboxtext"></span>
                <input type="button" id="messageboxok" onclick="game.messageBoxOK();">
                <input type="button" id="messageboxcancel" onclick="game.messageBoxCancel();">
            </div>
        </div>
        <div id="loadingscreen" class="gamelayer">
            <div id="loadingmessage"></div>
        </div>
    </div>
</body>

接下来,我们将把消息框的样式添加到 styles.css 中,如清单 8-27 所示。

清单 8-27。 消息框样式(styles.css)

/* Message Box Screen */
#messageboxscreen {
    background:rgba(0,0,0,0.7);
    z-index:20;
}
#messagebox {
    position:absolute;
    top:170px;
    left:140px;
    width:296px;
    height:178px;
    color:white;
    background:url(img/messagebox.png) no-repeat center;
    color:rgb(130,150,162);
    overflow:hidden;
    font-size: 13px;
    font-family: 'Courier New', Courier, monospace;
}
#messagebox span {
    position:absolute;
    top:30px;
    left:50px;
    width:200px;
    height:100px;
}

#messagebox input[type="button"]{
    background-image: url(img/buttons.png);
    position:absolute;
    border-width:0px;
    padding:0px;
}
#messageboxok{
    background-position: -2px -150px;
    top:126px;
    left:11px;
    width:74px;
    height:26px;
}
#messageboxok:active,#messageboxok:disabled{
    background-position: -2px -186px;
}

#messageboxcancel{
    background-position: -86px -150px;
    left:197px;
    top:129px;
    width:73px;
    height:24px;
}
#messageboxcancel:active,#messageboxcancel:disabled{
    background-position: -86px -184px;
}

最后,我们给游戏对象添加一些方法,如清单 8-28 所示。

清单 8-28。 给游戏对象添加消息框方法(game.js)

/* Message Box related code*/
messageBoxOkCallback:undefined,
messageBoxCancelCallback:undefined,
showMessageBox:function(message,onOK,onCancel){
    // Set message box text
    $('#messageboxtext').html(message);

    // Set message box ok and cancel handlers and enable buttons
    if(!onOK){
        game.messageBoxOkCallback = undefined;
    } else {
        game.messageBoxOkCallback = onOK;
    }
    if(!onCancel){
        game.messageBoxCancelCallback = undefined;
        $("#messageboxcancel").hide();
    } else {
        game.messageBoxCancelCallback = onCancel;
        $("#messageboxcancel").show();
    }

    // Display the message box and wait for user to click a button
    $('#messageboxscreen').show();
},
messageBoxOK:function(){
    $('#messageboxscreen').hide();
    if(game.messageBoxOkCallback){
        game.messageBoxOkCallback()
    }
},
messageBoxCancel:function(){
    $('#messageboxscreen').hide();
    if(game.messageBoxCancelCallback){
        game.messageBoxCancelCallback();
    }
},

showMessageBox()方法首先在 messageboxtext 元素中设置消息。接下来,它将 onOK 和 onCancel 回调方法参数保存到 messageBoxOkCallback 和 messageBoxCancelCallback 变量中。它根据是否传递了取消回调方法参数来显示或隐藏取消按钮。最后,它展示了 messageboxscreen 层。

messageBoxOK()和 messageBoxCancel()方法隐藏 messageboxscreen 层,然后调用它们各自的回调方法(如果已设置)。

在没有指定任何回调方法的情况下调用 showMessageBox()时,它会在一个只有 OK 按钮的黑屏上显示消息框,如图图 8-9 所示。

9781430247104_Fig08-09.jpg

图 8-9。消息框中显示的示例消息

现在消息框的代码已经就绪,我们将实现我们的游戏触发器。

实现触发器

我们的游戏将使用两种类型的触发器。

  • 定时触发器将在指定时间后执行操作。他们也可以定期重复。
  • 条件触发器将在指定的条件为真时执行操作。

我们将从在 maps 对象的级别中添加一个触发器数组开始,如清单 8-29 所示。

清单 8-29。 向关卡中添加触发器(maps.js)

/* Conditional and Timed Trigger Events */
"triggers":[
    /* Timed Events*/
    {"type":"timed","time":1000,
        "action":function(){
            game.showMessage("system","You have 20 seconds left.\nGet the harvester near the oil field.");
        }
    },
    {"type":"timed","time":21000,
        "action":function(){
            singleplayer.endLevel(false);
        }
    },
    /* Conditional Event */
    {"type":"conditional",
        "condition":function(){
            var transport = game.getItemByUid(-1);
            return (transport.x <10 && transport.y <10);
        },
        "action":function(){
            singleplayer.endLevel(true);
        }
    }
],

所有的触发器都有一个类型和一个动作方法。我们在数组中定义了三个触发器。

第一个触发器是时间设置为 1 秒的定时触发器。在它的动作参数中,我们调用 game.showMessage()并告诉玩家他有 20 秒的时间将收割机移动到油田附近。

第二个触发器计时 20 秒后,调用 singleplayer.endLevel()方法,参数为 false,表示任务失败。

最终触发器是条件触发器。当运输工具位于地图的左上角象限内且 x 和 y 坐标小于 10°时,condition 方法返回 true。当这个条件被触发时,action 方法调用 singleplayer.endLevel()方法,参数 true 表示任务成功完成。

接下来我们将在 singleplayer 对象中实现 endLevel()方法,如清单 8-30 所示。

清单 8-30。 实现 singleplayer endLevel()方法(singleplayer.js)

endLevel:function(success){
    clearInterval(game.animationInterval);
    game.end();

    if (success){
        var moreLevels = (singleplayer.currentLevel < maps.singleplayer.length-1);
        if (moreLevels){
            game.showMessageBox("Mission Accomplished.",function(){
                $('.gamelayer').hide();
                singleplayer.currentLevel++;
                singleplayer.startCurrentLevel();
            });
        } else {
            game.showMessageBox("Mission Accomplished.<br><br>This was the last mission in the campaign.<br><br>Thank You for playing.",function(){
                $('.gamelayer').hide();
                $('#gamestartscreen').show();
            });
        }
    } else {
        game.showMessageBox("Mission Failed.<br><br>Try again?",function(){
            $('.gamelayer').hide();
            singleplayer.startCurrentLevel();
        }, function(){
            $('.gamelayer').hide();
            $('#gamestartscreen').show();
        });
    }
}

我们首先清除调用 game.animationLoop()方法的 game.animationInterval 计时器。接下来我们调用 game.end()方法。

如果关卡成功完成,我们将检查地图中是否有更多的关卡。如果是这样,我们会在消息框中通知玩家任务成功,然后当玩家点击 OK 按钮时开始下一关。如果没有更多的关卡,我们会通知玩家,但是当玩家点击“确定”时,我们会返回到游戏开始菜单。

如果关卡没有成功完成,我们会询问玩家是否想再试一次。如果玩家点击确定,我们重新开始当前的水平。如果玩家点击取消,我们返回游戏开始菜单。

接下来我们将为游戏对象添加一些与触发器相关的方法,如清单 8-31 所示。

清单 8-31。 给游戏对象添加触发相关方法(game.js)

// Methods for handling triggered events within the game
initTrigger:function(trigger){
    if(trigger.type == "timed"){
        trigger.timeout = setTimeout (function(){
            game.runTrigger(trigger);
        },trigger.time)
    } else if(trigger.type == "conditional"){
        trigger.interval = setInterval (function(){
            game.runTrigger(trigger);
        },1000)
    }
},
runTrigger:function(trigger){
    if(trigger.type == "timed"){
        // Re initialize the trigger based on repeat settings
        if (trigger.repeat){
            game.initTrigger(trigger);
        }
        // Call the trigger action
        trigger.action(trigger);
    } else if (trigger.type == "conditional"){
        //Check if the condition has been satisfied
        if(trigger.condition()){
            // Clear the trigger
            game.clearTrigger(trigger);
            // Call the trigger action
            trigger.action(trigger);
        }
    }
},
clearTrigger:function(trigger){
    if(trigger.type == "timed"){
        clearTimeout(trigger.timeout);
    } else if (trigger.type == "conditional"){
        clearInterval(trigger.interval);
    }
},
end:function(){
    // Clear Any Game Triggers
    if (game.currentLevel.triggers){
        for (var i = game.currentLevel.triggers.length - 1; i >= 0; i--){
            game.clearTrigger(game.currentLevel.triggers[i]);
        };
    }
    game.running = false;
}

我们实现的第一个方法是 initTrigger()。我们检查触发器是定时的还是有条件的。对于定时触发器,我们在 time 参数中指定的超时之后调用 runTrigger()方法。对于条件触发器,我们每秒调用一次 runTrigger()方法。

在 runTrigger()方法中,我们检查触发器是定时的还是有条件的。对于指定了重复参数的定时触发器,我们再次调用 initTrigger()。然后,我们执行触发操作。对于条件触发器,我们检查条件是否为真。如果是,我们清除触发器并执行操作。

clearTimeout()方法只是清除触发器的超时或间隔。

最后,end()方法清除某个级别的所有触发器,并将 game.running 变量设置为 false。

我们要做的最后一个改变是游戏对象的 start()方法,如清单 8-32 所示。

清单 8-32。 初始化 start()方法(game.js)内的触发器

    start:function(){
        $('.gamelayer').hide();
        $('#gameinterfacescreen').show();
        game.running = true;
        game.refreshBackground = true;
        game.drawingLoop();

        $('#gamemessages').html("");
        // Initialize All Game Triggers
        for (var i = game.currentLevel.triggers.length - 1; i >= 0; i--){
            game.initTrigger(game.currentLevel.triggers[i]);
        };
    },

当我们开始关卡时,我们初始化 gamemessages 容器。接下来,我们遍历当前级别的触发器数组,并为每个触发器调用 initTrigger()。

如果我们现在运行游戏,我们应该会收到一条消息,要求我们在 20 秒内将收割机带到油田附近。如果我们没有及时这样做,我们会看到一个消息框,指示任务失败,如图图 8-10 所示。

9781430247104_Fig08-10.jpg

图 8-10。任务失败时显示的消息

如果我们点击“确定”按钮,关卡将重新开始,我们将返回到任务简报界面。如果我们点击取消按钮,我们将回到主菜单。

如果我们将收割机移向油田,并在 20 秒结束前到达那里,我们将看到一个消息框,指示任务已完成,如图图 8-11 所示。

9781430247104_Fig08-11.jpg

图 8-11。任务完成时显示的消息

由于这是我们战役中唯一的任务,我们将会看到战役结束的消息框。当我们单击“确定”时,我们将返回到主菜单。

摘要

我们在这一章完成了很多。我们从创建一个基础经济开始,在那里我们可以通过收割来赚取现金。然后我们实现了使用工具条上的按钮来购买星港的单位和基地的建筑的能力。

我们开发了一个消息系统和一个消息对话框来和玩家交流。然后,我们为基于触发器的动作构建了一个系统,处理定时和条件触发器。最后,我们使用这些触发器来创建一个简单的任务目标和任务成功或失败的标准。尽管这是一个相当简单的任务,但我们现在已经有了建造更复杂关卡的基础设施。

在下一章,我们将处理游戏的另一个重要组成部分:战斗。我们将对单位和炮塔实现不同的基于攻击的命令状态。我们将使用触发器和命令状态的组合来使单位在战斗中表现得聪明。最后,我们将着眼于实现战争迷雾,使单位看不到或攻击未探索的领域。

九、添加武器和战役

在过去的几章中,我们建立了游戏的基本框架;增加了车辆、飞机和建筑物等实体;执行单位移动;并使用侧边栏创建了一个简单的经济。我们现在有一个游戏,我们可以开始水平,赚钱,购买建筑物和单位,并移动这些单位来实现简单的目标。

在这一章中,我们将实现车辆、飞机和炮塔的武器。我们将增加处理基于战斗的命令的能力,例如攻击、守卫、巡逻和狩猎,让单位以智能的方式战斗。最后,我们将实现一个限制地图可见性的战争迷雾,允许有趣的策略,如偷袭和伏击。

我们开始吧。我们将使用第八章中的代码作为起点。

实现战斗系统

我们的游戏将有一个相当简单的战斗系统。所有单位和炮塔都有自己的武器和子弹类型。当攻击敌人时,单位将首先进入射程,转向目标,然后向他们发射子弹。一旦单位发射了一颗子弹,它会等到它的武器重新加载后再发射。

子弹本身会是一个独立的游戏实体,有自己的动画逻辑。发射时,子弹会飞向目标,一旦到达目的地就会爆炸。

我们要做的第一件事是给我们的游戏添加子弹。

添加项目符号

我们将从在 bullets.js 中定义一个新的 bullets 对象开始,如清单 9-1 所示。

清单 9-1。 定义子弹对象(bullets.js)

var bullets = {
    list:{
        "fireball":{
            name:"fireball",
            speed:60,
            reloadTime:30,
            range:8,
            damage:10,
            spriteImages:[
                {name:"fly",count:1,directions:8},
                {name:"explode",count:7}
            ],
        },
        "heatseeker":{
            name:"heatseeker",
            reloadTime:40,
            speed:25,
            range:9,
            damage:20,
            turnSpeed:2,
            spriteImages:[
                {name:"fly",count:1,directions:8},
                {name:"explode",count:7}
            ],
        },
        "cannon-ball":{
            name:"cannon-ball",
            reloadTime:40,
            speed:25,
            damage:10,
            range:6,
            spriteImages:[
                {name:"fly",count:1,directions:8},
                {name:"explode",count:7}
            ],
        },
        "bullet":{
            name:"bullet",
            damage:5,
            speed:50,
            range:5,
            reloadTime:20,
            spriteImages:[
                {name:"fly",count:1,directions:8},
                {name:"explode",count:3}
            ],
        },
    },
    defaults:{
        type:"bullets",
        distanceTravelled:0,
        animationIndex:0,
        direction:0,
        directions:8,
        pixelWidth:10,
        pixelHeight:11,
        pixelOffsetX:5,
        pixelOffsetY:5,
        radius:6,
        action:"fly",
        selected:false,
        selectable:false,
        orders:{type:"fire"},
        moveTo:function(destination){
            // Weapons like the heatseeker can turn slowly toward target while moving
            if (this.turnSpeed){
                // Find out where we need to turn to get to destination
                var newDirection = findFiringAngle(destination,this,this.directions);
                // Calculate difference between new direction and current direction
                var difference = angleDiff(this.direction,newDirection,this.directions);
                // Calculate amount that bullet can turn per animation cycle
                var turnAmount = this.turnSpeed*game.turnSpeedAdjustmentFactor;
                if (Math.abs(difference)>turnAmount){
                    this.direction = wrapDirection(this.direction+turnAmount*Math.abs(difference)/difference,this.directions);
                }
            }

            var movement = this.speed*game.speedAdjustmentFactor;
            this.distanceTravelled += movement;

            var angleRadians = −((this.direction)/this.directions)*2*Math.PI ;

            this.lastMovementX = − (movement*Math.sin(angleRadians));
            this.lastMovementY = − (movement*Math.cos(angleRadians));
            this.x = (this.x +this.lastMovementX);
            this.y = (this.y +this.lastMovementY);
        },
        reachedTarget:function(){
            var item = this.target;
            if (item.type=="buildings"){
                return (item.x<= this.x && item.x >= this.x - item.baseWidth/game.gridSize &&
item.y<= this.y && item.y >= this.y - item.baseHeight/game.gridSize);
            } else if (item.type=="aircraft"){
                return (Math.pow(item.x-this.x,2)+Math.pow(item.y-(this.y+item.pixelShadowHeight/
game.gridSize),2) < Math.pow((item.radius)/game.gridSize,2));
           } else {
                   return (Math.pow(item.x-this.x,2)+Math.pow(item.y-this.y,2) < Math.pow((item.radius)/game.gridSize,2));
           }
        },
        processOrders:function(){
            this.lastMovementX = 0;
            this.lastMovementY = 0;
            switch (this.orders.type){
                case "fire":
                    // Move toward destination and stop when close by or if travelled past range
                    var reachedTarget = false;
                    if (this.distanceTravelled>this.range
                        || (reachedTarget = this.reachedTarget())) {
                        if(reachedTarget){
                            this.target.life -= this.damage;
                            this.orders = {type:"explode"};
                            this.action = "explode";
                            this.animationIndex = 0;
                        } else {
                            // Bullet fizzles out without hitting target
                            game.remove(this);
                        }
                    } else {
                        this.moveTo(this.target);
                    }
                    break;
            }
        },
        animate:function(){
            switch (this.action){
                case "fly":
                    var direction = wrapDirection(Math.round(this.direction),this.directions);
                     this.imageList = this.spriteArray["fly-"+ direction];
                    this.imageOffset = this.imageList.offset;
                    break;
                case "explode":
                    this.imageList = this.spriteArray["explode"];
                    this.imageOffset = this.imageList.offset + this.animationIndex;
                    this.animationIndex++;
                    if (this.animationIndex>=this.imageList.count){
                        // Bullet explodes completely and then disappears
                        game.remove(this);
                    }
                    break;
            }
        },
        draw:function(){
            var x = (this.x*game.gridSize)-game.offsetX-this.pixelOffsetX + this.lastMovementX*game.drawingInterpolationFactor*game.gridSize;
            var y = (this.y*game.gridSize)-game.offsetY-this.pixelOffsetY + this.lastMovementY*game.drawingInterpolationFactor*game.gridSize;
            var colorOffset = 0;
            game.foregroundContext.drawImage(this.spriteSheet, this.imageOffset*this.pixelWidth,
colorOffset, this.pixelWidth,this.pixelHeight, x,y,this.pixelWidth,this.pixelHeight);
        }
    },
    load:loadItem,
    add:addItem,
}

子弹对象遵循与所有其他游戏实体相同的模式。我们从定义四种子弹类型开始:火球、热探测器、炮弹和子弹。每个项目符号都有一组共同的属性。

  • 速度:子弹行进的速度
  • reloadTime:在子弹再次发射之前,发射后动画循环的次数
  • 伤害:子弹爆炸时对目标造成的伤害
  • 射程:子弹失去动量前的最大飞行距离

项目符号还定义了两个动画序列:飞行和爆炸。飞行状态有八个方向,类似于车辆和飞机。分解状态只有方向,但有多个帧。

然后我们定义一个默认的 moveTo()方法,它类似于 aircraft moveTo()方法。在这个方法中,我们首先检查子弹是否可以转动,如果可以,使用 findFiringAngle()方法计算子弹朝向目标中心的角度,轻轻地将子弹转向目标。接下来,我们沿着当前方向向前移动项目符号,并更新项目符号的 distanceTravelled 属性。

接下来我们定义一个 reachedTarget()方法来检查子弹是否已经到达目标。我们检查子弹的坐标是否在建筑物的基准区域内,是否在车辆和飞机的项目半径内。如果是这样,我们返回 true 值。

在 processOrders()方法中,我们实现了 fire order。我们检查子弹是否到达了目标或者超出了射程。如果没有,我们继续将子弹移向目标。

如果子弹超出射程而没有击中目标,我们将它从游戏中移除。如果子弹到达目标,我们首先将子弹的顺序和动画状态设置为爆炸,并按伤害量减少其目标的生命。

在 animate()方法中,一旦分解动画序列完成,我们就移除子弹。

现在我们已经定义了 bullets 对象,我们将在 index.html 的部分添加对 bullets.js 的引用,如清单 9-2 所示。

清单 9-2。 【添加对项目符号对象的引用】(index.html)

<script src="js/bullets.js" type="text/javascript" charset="utf-8"></script>

我们还将在 common.js 中定义 findFiringAngle()方法,如清单 9-3 所示。

清单 9-3。 定义 findFiringAngle()方法(common.js)

function findFiringAngle(target,source,directions){
    var dy = (target.y) - (source.y);
    var dx = (target.x) - (source.x);

    if(target.type=="buildings"){
        dy += target.baseWidth/2/game.gridSize;
        dx += target.baseHeight/2/game.gridSize;
    } else if(target.type == "aircraft"){
        dy -= target.pixelShadowHeight/game.gridSize;
    }

     if(source.type=="buildings"){
        dy -= source.baseWidth/2/game.gridSize;
        dx -= source.baseHeight/2/game.gridSize;
    } else if(source.type == "aircraft"){
        dy += source.pixelShadowHeight/game.gridSize;
    }

    //Convert Arctan to value between (0 – 7)
    var angle = wrapDirection(directions/2-(Math.atan2(dx,dy)*directions/(2*Math.PI)),directions);
    return angle;
}

findFiringAngle()方法类似于 findAngle()方法,除了我们调整 dy 和 dx 变量的值以指向源和目标的中心。对于建筑物,我们使用 baseWidth 和 baseHeight 属性调整 dx 和 dy,对于飞机,我们通过 pixelShadowHeight 属性调整 dy。这样子弹就可以瞄准目标的中心。

我们还将修改 common.js 中的 loadItem()方法,以便在项目加载时加载项目符号,如清单 9-4 所示。

清单 9-4。 加载物品时加载子弹(common.js)

/* The default load() method used by all our game entities*/
function loadItem(name){
    var item = this.list[name];
    // if the item sprite array has already been loaded then no need to do it again
    if(item.spriteArray){
        return;
    }
    item.spriteSheet = loader.loadImage('img/'+this.defaults.type+'/'+name+'.png');
    item.spriteArray = [];
    item.spriteCount = 0;

    for (var i=0; i < item.spriteImages.length; i++){
        var constructImageCount = item.spriteImages[i].count;
        var constructDirectionCount = item.spriteImages[i].directions;
        if (constructDirectionCount){
            for (var j=0; j < constructDirectionCount; j++) {
                var constructImageName = item.spriteImages[i].name +"-"+j;
                item.spriteArray[constructImageName] = {
                    name:constructImageName,
                    count:constructImageCount,
                    offset:item.spriteCount
                };
                item.spriteCount += constructImageCount;
            };
        } else {
            var constructImageName = item.spriteImages[i].name;
            item.spriteArray[constructImageName] = {
                name:constructImageName,
                count:constructImageCount,
                offset:item.spriteCount
            };
            item.spriteCount += constructImageCount;
        }
    };
    // Load the weapon if item has one
    if(item.weaponType){
        bullets.load(item.weaponType);
    }
}

当加载一个项目时,我们检查它是否定义了 weaponType 属性,如果是,使用 bullets.load()方法加载武器的项目符号。所有有攻击能力的实体都有一个武器类型属性。

我们要做的下一个改变是修改游戏对象的 drawingLoop()方法,以在游戏中所有其他项目的顶部绘制子弹和爆炸。更新后的 drawingLoop()方法将类似于清单 9-5 中的。

清单 9-5。 修改 drawingLoop()在其他项目上方绘制项目符号(game.js)

drawingLoop:function(){
    // Handle Panning the Map
    game.handlePanning();

    // Check the time since the game was animated and calculate a linear interpolation factor (−1 to 0)
    // since drawing will happen more often than animation
    game.lastDrawTime = (new Date()).getTime();
    if (game.lastAnimationTime){
        game.drawingInterpolationFactor = (game.lastDrawTime -game.lastAnimationTime)/game.animationTimeout - 1;
        if (game.drawingInterpolationFactor>0){ // No point interpolating beyond the next animation loop...
            game.drawingInterpolationFactor = 0;
        }
    } else {
        game.drawingInterpolationFactor = −1;
    }
    // Since drawing the background map is a fairly large operation,
    // we only redraw the background if it changes (due to panning)
    if (game.refreshBackground){
        game.backgroundContext.drawImage(game.currentMapImage,game.offsetX,game.offsetY, game.canvasWidth, game.canvasHeight, 0,0,game.canvasWidth,game.canvasHeight);
        game.refreshBackground = false;
    }

    // Clear the foreground canvas
    game.foregroundContext.clearRect(0,0,game.canvasWidth,game.canvasHeight);

    // Start drawing the foreground elements
    for (var i = game.sortedItems.length - 1; i >= 0; i--){
        if (game.sortedItems[i].type != "bullets"){
            game.sortedItems[i].draw();
        }
    };

    // Draw the bullets on top of all the other elements
    for (var i = game.bullets.length - 1; i >= 0; i--){
        game.bullets[i].draw();
    };

    // Draw the mouse
    mouse.draw()

    // Call the drawing loop for the next frame using request animation frame
    if (game.running){
        requestAnimationFrame(game.drawingLoop);
    }
},

我们先画出所有不是子弹的项目,最后画出子弹。这样,子弹和爆炸在游戏中总是清晰可见的。

最后,我们将修改游戏对象的 resetArrays()方法来重置 game.bullets[]数组,如清单 9-6 所示。

清单 9-6。 重置 resetArrays()(game.js)内的子弹数组

resetArrays:function(){
    game.counter = 1;
    game.items = [];
    game.sortedItems = [];
    game.buildings = [];
    game.vehicles = [];
    game.aircraft = [];
    game.terrain = [];
    game.triggeredEvents = [];
    game.selectedItems = [];
    game.sortedItems = [];
    game.bullets = [];
},

现在我们已经实现了子弹对象,是时候为炮塔、车辆和飞机实现基于战斗的命令了。

基于战斗的炮塔订单

地面炮塔可以向任何地面威胁发射炮弹。当处于守卫或攻击模式时,他们将搜索视线内的有效目标,将炮塔对准目标,并发射子弹,直到目标被摧毁或超出射程。

我们将通过修改 buildings.js 中的地面炮塔对象来实现 processOrders()方法,如清单 9-7 所示。

清单 9-7。 修改地堡对象实现攻击(buildings.js)

isValidTarget:isValidTarget,
findTargetsInSight:findTargetsInSight,
processOrders:function(){
    if(this.reloadTimeLeft){
        this.reloadTimeLeft--;
    }
    // damaged turret cannot attack
    if(this.lifeCode != "healthy"){
        return;
    }
    switch (this.orders.type){
        case "guard":
            var targets = this.findTargetsInSight();
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0]};
            }
            break;
        case "attack":
            if(!this.orders.to ||
                this.orders.to.lifeCode == "dead" ||
                !this.isValidTarget(this.orders.to) ||
                (Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))>Math.pow(this.sight,2)
                ){

                var targets = this.findTargetsInSight();
                if(targets.length>0){
                    this.orders.to = targets[0];
                } else {
                    this.orders = {type:"guard"};
                }
            }

            if (this.orders.to){
                var newDirection = findFiringAngle(this.orders.to,this,this.directions);
                var difference = angleDiff(this.direction,newDirection,this.directions);
                var turnAmount = this.turnSpeed*game.turnSpeedAdjustmentFactor;
                if (Math.abs(difference)>turnAmount){
                    this.direction = wrapDirection(this.direction+turnAmount*Math.abs(difference)/difference,this.directions);
                    return;
                } else {
                    this.direction = newDirection;
                    if(!this.reloadTimeLeft){
                        this.reloadTimeLeft = bullets.list[this.weaponType].reloadTime;
                        var angleRadians = −(Math.round(this.direction)/this.directions)*2*Math.PI ;
                        var bulletX = this.x+0.5- (1*Math.sin(angleRadians));
                        var bulletY = this.y+0.5- (1*Math.cos(angleRadians));
                        var bullet = game.add({name:this.weaponType,type:"bullets", x:bulletX,
y:bulletY, direction:this.direction, target:this.orders.to});
                    }
                }
            }
            break;
    }
}

我们首先在地面炮塔对象内部分配两个名为 isValidTarget()和 findTargetInSight()的方法。我们需要定义这些方法。然后我们定义 processOrders()方法。

在 processOrders()方法中,如果 reloadTimeLeft 属性已定义并且大于 0,我们将减小该属性的值。如果转台生命码不健康(它已损坏或失效),我们什么也不做并退出。

接下来,我们定义守卫命令和攻击命令的行为。在守卫模式下,我们使用 findTargetsInSight()方法来查找目标,如果找到了,就攻击它。在攻击模式下,如果炮塔的当前目标是未定义的、死亡的或看不见的,我们使用 findTargetsInSight()找到新的有效目标,并设置攻击它的顺序。如果我们找不到一个有效的目标,我们回到守卫模式。

如果炮塔有一个有效的目标,我们把它转向目标。一旦炮塔面向目标并且 reloadTimeLeft 为 0,我们通过使用 game.add()方法将子弹添加到游戏中来发射子弹,并将炮塔的 reloadTimeLeft 属性重置为子弹的重新加载时间。

接下来,我们将修改默认 animate()方法中的守卫动画案例来处理方向,如清单 9-8 所示。

清单 9-8。 修改内护箱 animate() (buildings.js)

case "guard":
    if (this.lifeCode == "damaged"){
        // The damaged turret has no directions
        this.imageList = this.spriteArray[this.lifeCode];
    } else {
        // The healthy turret has 8 directions
        var direction = wrapDirection(Math.round(this.direction),this.directions);
        this.imageList = this.spriteArray[this.lifeCode+"-"+ direction];
    }
    this.imageOffset = this.imageList.offset;
    break;

接下来,我们将在 common.js 中添加两个名为 isValidTarget()和 findTargetInSight()的方法,如清单 9-9 所示。

清单 9-9。 添加 isValidTarget()和 findTargetInSight()方法(common.js)

// Common Functions related to combat
function isValidTarget(item){
    return item.team != this.team &&
(this.canAttackLand && (item.type == "buildings" || item.type == "vehicles")||
(this.canAttackAir && (item.type == "aircraft")));
}

function findTargetsInSight(increment){
    if(!increment){
        increment=0;
    }
    var targets = [];
    for (var i = game.items.length - 1; i >= 0; i--){
        var item = game.items[i];
        if (this.isValidTarget(item)){
            if(Math.pow(item.x-this.x,2) + Math.pow(item.y-this.y,2)<Math.pow(this.sight+increment,2)){
                targets.push(item);
            }
        }
    };

    // Sort targets based on distance from attacker
    var attacker = this;
    targets.sort(function(a,b){
        return (Math.pow(a.x-attacker.x,2) + Math.pow(a.y-attacker.y,2))-(Math.pow(b.x-attacker.x,2) + Math.pow(b.y-attacker.y,2));
       });

    return targets;
}

isValidTarget()方法如果目标物品来自对方队伍,则返回 true,可以攻击。

findTargetsInSight()方法检查 game.items()数组中的所有项目,查看它们是否是有效的目标并在范围内,如果是,它将它们添加到 targets 数组中。然后,它根据每个目标与攻击者的距离对目标数组进行排序。该方法还接受一个可选的 increment 参数,该参数允许我们找到超出项目范围的目标。这两种常用的方法将被炮塔、车辆和飞机使用。

在我们看到代码的结果之前,我们将通过修改触发器和项目数组从最后一级更新我们的映射,如清单 9-10 所示。

清单 9-10。 更新地图条目和触发器(maps.js)

/* Entities to be added */
"items":[
    {"type":"buildings","name":"base","x":11,"y":14,"team":"blue"},
    {"type":"buildings","name":"starport","x":18,"y":14,"team":"blue"},

    {"type":"vehicles","name":"harvester","x":16,"y":12,"team":"blue","direction":3},
    {"type":"terrain","name":"oilfield","x":3,"y":5,"action":"hint"},

    {"type":"terrain","name":"bigrocks","x":19,"y":6},
    {"type":"terrain","name":"smallrocks","x":8,"y":3},

    {"type":"vehicles","name":"scout-tank","x":26,"y":14,"team":"blue","direction":4},
    {"type":"vehicles","name":"heavy-tank","x":26,"y":16,"team":"blue","direction":5},
    {"type":"aircraft","name":"chopper","x":20,"y":12,"team":"blue","direction":2},
    {"type":"aircraft","name":"wraith","x":23,"y":12,"team":"blue","direction":3},

    {"type":"buildings","name":"ground-turret","x":15,"y":23,"team":"green"},
    {"type":"buildings","name":"ground-turret","x":20,"y":23,"team":"green"},

    {"type":"vehicles","name":"scout-tank","x":16,"y":26,"team":"green","direction":4},
    {"type":"vehicles","name":"heavy-tank","x":18,"y":26,"team":"green","direction":6},
    {"type":"aircraft","name":"chopper","x":20,"y":27,"team":"green","direction":2},
    {"type":"aircraft","name":"wraith","x":22,"y":28,"team":"green","direction":3},

    {"type":"buildings","name":"base","x":19,"y":28,"team":"green"},
    {"type":"buildings","name":"starport","x":15,"y":28,"team":"green"},
],

/* Conditional and Timed Trigger Events */
"triggers":[
],

我们移除了在上一章中定义的触发器,因此关卡不会在 30 秒后结束。现在,如果我们在浏览器中运行游戏,移动一辆车辆靠近敌人的炮塔,炮塔应该会开始攻击车辆,如图图 9-1 所示。

9781430247104_Fig09-01.jpg

图 9-1。炮塔向射程内的车辆开火

子弹击中车辆时会爆炸,缩短车辆寿命。一旦车辆失去所有生命,它就会从游戏中消失。如果目标超出射程,炮塔停止向目标射击,并移动到下一个目标。

接下来,我们将实现基于战斗的飞机订单。

基于战斗的飞机订单

我们将为飞机定义几个基本的基于战斗的命令状态。

  • 攻击:在目标范围内移动并射击它。
  • 漂浮:呆在一个地方,攻击任何靠近的敌人。
  • 守卫:跟随一个友军单位,向任何靠近的敌人射击。
  • 狩猎:积极寻找地图上任何地方的敌人并攻击他们。
  • 巡逻:在两点之间移动,向任何进入射程的敌人射击。
  • 哨兵:呆在一个地方,比在漂浮模式下攻击敌人更有侵略性。

我们将通过修改 aircraft 对象中默认的 processOrders()方法来实现这些状态,如清单 9-11 中的所示。

清单 9-11。 对战机实现战斗命令(aircraft.js)

isValidTarget:isValidTarget,
findTargetsInSight:findTargetsInSight,
processOrders:function(){
    this.lastMovementX = 0;
    this.lastMovementY = 0;
    if(this.reloadTimeLeft){
        this.reloadTimeLeft--;
    }
    switch (this.orders.type){
        case "float":
            var targets = this.findTargetsInSight();
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0]};
            }
            break;
        case "sentry":
            var targets = this.findTargetsInSight(2);
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
            }
            break;
        case "hunt":
            var targets = this.findTargetsInSight(100);
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
            }
            break;
        case "move":
            // Move toward destination until distance from destination is less than aircraft radius
            var distanceFromDestinationSquared = (Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2));
            if (distanceFromDestinationSquared < Math.pow(this.radius/game.gridSize,2)) {
                this.orders = {type:"float"};
            } else {
                this.moveTo(this.orders.to);
            }
            break;
        case "attack":
            if(this.orders.to.lifeCode == "dead" || !this.isValidTarget(this.orders.to)){
                if (this.orders.nextOrder){
                    this.orders = this.orders.nextOrder;
                } else {
                    this.orders = {type:"float"};
                }
                return;
            }
            if ((Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.sight,2)) {
                //Turn toward target and then start attacking when within range of the target
                var newDirection = findFiringAngle(this.orders.to,this,this.directions);
                var difference = angleDiff(this.direction,newDirection,this.directions);
                var turnAmount = this.turnSpeed*game.turnSpeedAdjustmentFactor;
                if (Math.abs(difference)>turnAmount){
                    this.direction = wrapDirection(this.direction+ turnAmount*Math.abs(difference)/difference, this.directions);
                    return;
                } else {
                    this.direction = newDirection;
                    if(!this.reloadTimeLeft){
                        this.reloadTimeLeft = bullets.list[this.weaponType].reloadTime;
                        var angleRadians = −(Math.round(this.direction)/this.directions)*2*Math.PI ;
                        var bulletX = this.x- (this.radius*Math.sin(angleRadians)/game.gridSize);
                        var bulletY = this.y- (this.radius*Math.cos(angleRadians)/game.gridSize)-this.pixelShadowHeight/game.gridSize;
                        var bullet = game.add({name:this.weaponType, type:"bullets",x:bulletX, y:bulletY, direction:newDirection, target:this.orders.to});
                    }
                }

            } else {
                var moving = this.moveTo(this.orders.to);
            }
            break;
        case "patrol":
            var targets = this.findTargetsInSight(1);
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
                return;
            }
            if ((Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.radius/game.gridSize,2)) {
                var to = this.orders.to;
                this.orders.to = this.orders.from;
                this.orders.from = to;
            } else {
                this.moveTo(this.orders.to);
            }
            break;
        case "guard":
            if(this.orders.to.lifeCode == "dead"){
                if (this.orders.nextOrder){
                    this.orders = this.orders.nextOrder;
                } else {
                    this.orders = {type:"float"};
                }
                return;
            }
            if ((Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.sight-2,2)) {
                var targets = this.findTargetsInSight(1);
                if(targets.length>0){
                    this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
                    return;
                }
            } else {
                this.moveTo(this.orders.to);
            }
            break;

    }
},

我们首先分配 isValidTarget()和 findTargetInSight()方法。然后,我们定义 processOrders()方法中的所有状态。

在 processOrders()方法中,我们减少了 reloadTimeLeft 属性的值,就像我们对炮塔所做的那样。然后,我们为每个订单状态定义案例。

如果订单类型是 float,我们使用 findTargetsInSight()来检查目标是否在附近,如果是,就攻击它。当订单类型是 sentry 时,我们做同样的事情,除了我们传递一个范围增量参数 2,以便飞机攻击稍微超出其典型范围的单位。

除了范围增量参数为 100 之外,寻线情况非常相似,这应该理想地覆盖整个地图。这意味着飞机将攻击地图上的任何敌人单位或车辆,从最近的开始。

对于攻击情况,我们首先检查目标是否还活着。如果没有,我们要么将 orders 设置为 orders.nextOrder(如果它已定义),要么返回浮点模式。

接下来我们检查目标是否在射程内,如果不在,我们就向目标靠近。接下来,我们确保飞机指向目标。最后,我们等到 reloadTimeLeft 变量为 0,然后向目标射出一颗子弹。

巡逻案件是移动和岗哨案件的结合。我们将飞机移动到 to 属性中定义的位置,一旦飞机到达该位置,就掉头向 from 位置移动。如果目标进入射程,我们将下一个攻击顺序设置为当前顺序。这样,如果飞机在巡逻时看到敌人,它会先攻击敌人,然后在敌人被消灭后再回去巡逻。

最后,在守卫模式下,我们将飞机移动到它所守卫的单位的视线范围内,并攻击任何靠近的敌人。

如果你运行我们到目前为止的代码,你应该可以看到不同的飞机互相攻击,如图图 9-2 所示。

9781430247104_Fig09-02.jpg

图 9-2。互相攻击的飞机

选择飞机后右击鼠标可以命令飞机攻击敌人或守卫朋友。直升机可以攻击地面和空中单位,而幽灵只能攻击空中单位。

我们通常会使用哨兵,猎人和巡逻的命令来给电脑人工智能一点优势,并使游戏对玩家更具挑战性。玩家将无法访问这些订单。

image

接下来,我们将实现基于战斗的车辆订单。

基于战斗的车辆订单

车辆的基于战斗的命令状态将非常类似于飞机的命令状态。

  • 攻击:在目标范围内移动并射击它。
  • 站立:呆在一个地方,攻击任何靠近的敌人。
  • 守卫:跟随一个友军单位,向任何靠近的敌人射击。
  • 狩猎:积极寻找地图上任何地方的敌人并攻击他们。
  • 巡逻:在两点之间移动,向任何进入射程的敌人射击。
  • 哨兵:呆在一个地方,比站着的时候攻击敌人更有侵略性。

我们将通过修改 vehicles 对象中默认的 processOrders()方法来实现这些状态,如清单 9-12 所示。

清单 9-12。 执行战斗命令的车辆(vehicles.js)

isValidTarget:isValidTarget,
findTargetsInSight:findTargetsInSight,
processOrders:function(){
    this.lastMovementX = 0;
    this.lastMovementY = 0;
    if(this.reloadTimeLeft){
        this.reloadTimeLeft--;
    }
    var target;
    switch (this.orders.type){
        case "move":
            // Move toward destination until distance from destination is less than vehicle radius
            var distanceFromDestinationSquared = (Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2));
            if (distanceFromDestinationSquared < Math.pow(this.radius/game.gridSize,2)) {
                //Stop when within one radius of the destination
                this.orders = {type:"stand"};
                return;
            } else if (distanceFromDestinationSquared <Math.pow(this.radius*3/game.gridSize,2)) {
                //Stop when within 3 radius of the destination if colliding with something
                this.orders = {type:"stand"};
                return;
            } else {
                if (this.colliding && (Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.radius*5/game.gridSize,2)) {
                    // Count collsions within 5 radius distance of goal
                    if (!this.orders.collisionCount){
                        this.orders.collisionCount = 1
                    } else {
                        this.orders.collisionCount ++;
                    }
                    // Stop if more than 30 collisions occur
                    if (this.orders.collisionCount > 30) {
                        this.orders = {type:"stand"};
                        return;
                    }
                }
                var moving = this.moveTo(this.orders.to);
                // Pathfinding couldn't find a path so stop
                if(!moving){
                    this.orders = {type:"stand"};
                    return;
                }
            }
            break;
        case "deploy":
            // If oilfield has been used already, then cancel order
            if(this.orders.to.lifeCode == "dead"){
                this.orders = {type:"stand"};
                return;
            }
            // Move to middle of oil field
            var target = {x:this.orders.to.x+1,y:this.orders.to.y+0.5,type:"terrain"};
            var distanceFromTargetSquared = (Math.pow(target.x-this.x,2) + Math.pow(target.y-this.y,2));
            if (distanceFromTargetSquared<Math.pow(this.radius*2/game.gridSize,2)) {
                // After reaching oil field, turn harvester to point toward left (direction 6)
                var difference = angleDiff(this.direction,6,this.directions);
                var turnAmount = this.turnSpeed*game.turnSpeedAdjustmentFactor;
                if (Math.abs(difference)>turnAmount){
                    this.direction = wrapDirection(this.direction+turnAmount*Math.abs(difference)/difference,this.directions);
                } else {
                    // Once it is pointing to the left, remove the harvester and oil field and deploy a harvester building
                    game.remove(this.orders.to);
                    this.orders.to.lifeCode="dead";
                    game.remove(this);
                    this.lifeCode="dead";
                    game.add({type:"buildings", name:"harvester", x:this.orders.to.x, y:this.orders.to.y, action:"deploy", team:this.team});
                }
            } else {
                var moving = this.moveTo(target);
                // Pathfinding couldn't find a path so stop
                if(!moving){
                    this.orders = {type:"stand"};
                }
            }
            break;
        case "stand":
            var targets = this.findTargetsInSight();
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0]};
            }
            break;
        case "sentry":
            var targets = this.findTargetsInSight(2);
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
            }
            break;
        case "hunt":
            var targets = this.findTargetsInSight(100);
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
            }
            break;
        case "attack":
            if(this.orders.to.lifeCode == "dead" || !this.isValidTarget(this.orders.to)){
                if (this.orders.nextOrder){
                    this.orders = this.orders.nextOrder;
                } else {
                    this.orders = {type:"stand"};
                }
                return;
            }
            if ((Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.sight,2)) {
                //Turn toward target and then start attacking when within range of the target
                var newDirection = findFiringAngle(this.orders.to,this,this.directions);
                var difference = angleDiff(this.direction,newDirection,this.directions);
                var turnAmount = this.turnSpeed*game.turnSpeedAdjustmentFactor;
                if (Math.abs(difference)>turnAmount){
                    this.direction = wrapDirection(this.direction + turnAmount*Math.abs(difference)/difference, this.directions);
                    return;
                } else {
                    this.direction = newDirection;
                    if(!this.reloadTimeLeft){
                        this.reloadTimeLeft = bullets.list[this.weaponType].reloadTime;
                        var angleRadians = −(Math.round(this.direction)/this.directions)*2*Math.PI ;
                        var bulletX = this.x- (this.radius*Math.sin(angleRadians)/game.gridSize);
                        var bulletY = this.y- (this.radius*Math.cos(angleRadians)/game.gridSize);
                        var bullet = game.add({name:this.weaponType,type:"bullets",x:bulletX,y:bulletY, direction:newDirection, target:this.orders.to});
                    }
                }
            } else {
                var moving = this.moveTo(this.orders.to);
                // Pathfinding couldn't find a path so stop
                if(!moving){
                    this.orders = {type:"stand"};
                    return;
                }
            }
            break;
        case "patrol":
            var targets = this.findTargetsInSight(1);
            if(targets.length>0){
                this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
                return;
            }
            if ((Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.radius*4/game.gridSize,2)) {
                var to = this.orders.to;
                this.orders.to = this.orders.from;
                this.orders.from = to;
            } else {
                this.moveTo(this.orders.to);
            }
            break;
        case "guard":
            if(this.orders.to.lifeCode == "dead"){
                if (this.orders.nextOrder){
                    this.orders = this.orders.nextOrder;
                } else {
                    this.orders = {type:"stand"};
                }
                return;
            }
            if ((Math.pow(this.orders.to.x-this.x,2) + Math.pow(this.orders.to.y-this.y,2))<Math.pow(this.sight-2,2)) {
                var targets = this.findTargetsInSight(1);
                if(targets.length>0){
                    this.orders = {type:"attack",to:targets[0],nextOrder:this.orders};
                    return;
                }
            } else {
                this.moveTo(this.orders.to);
            }
            break;
    }
},

状态的实现几乎与飞机相同。如果我们现在运行代码,我们应该能够攻击车辆,如图 9-3 所示。

9781430247104_Fig09-03.jpg

图 9-3。用车辆攻击

我们现在可以用车辆、飞机或炮塔攻击。

你可能已经注意到,当你靠近时,对方的单位会攻击,但他们仍然很容易被击败。现在战斗系统已经就位,我们将探索如何让敌人变得更聪明,让游戏更有挑战性。

构建智能敌人

建造智能敌人 AI 的主要目标是确保玩游戏的人发现它相当具有挑战性,并且有完成关卡的乐趣。关于 RTS 游戏,尤其是单人战役,需要意识到的一个重要的事情是,敌人 AI 不需要是特级大师级别的棋手。事实是,我们可以只使用战斗命令状态和条件脚本事件的组合来为玩家提供非常引人注目的体验。

通常,人工智能的“智能”行为方式会随着每个等级的不同而不同。

在一个简单的关卡中,没有生产设施,只有地面单位,唯一可能的行为就是开到敌人单位前并攻击他们。巡逻和哨兵命令的结合通常足以达到这个目的。我们也可以通过在特定时间或特定事件发生时(例如,当玩家到达特定地点或建造特定建筑时)攻击玩家来增加关卡的趣味性。

在一个更复杂的层面上,我们可以通过使用定时触发和狩猎命令,在特定的时间间隔内制造和派出一波又一波的敌人来挑战敌人。

我们可以通过在地图上添加更多的项目和触发器来看到这些想法在起作用,如清单 9-13 中的所示。

清单 9-13。 增加触发器和物品使关卡更具挑战性(maps.js)

/* Entities to be added */
"items":[
    {"type":"buildings","name":"base","x":11,"y":14,"team":"blue"},
    {"type":"buildings","name":"starport","x":18,"y":14,"team":"blue"},

    {"type":"vehicles","name":"harvester","x":16,"y":12,"team":"blue","direction":3},
    {"type":"terrain","name":"oilfield","x":3,"y":5,"action":"hint"},

    {"type":"terrain","name":"bigrocks","x":19,"y":6},
    {"type":"terrain","name":"smallrocks","x":8,"y":3},

    {"type":"vehicles","name":"scout-tank","x":26,"y":14,"team":"blue","direction":4},
    {"type":"vehicles","name":"heavy-tank","x":26,"y":16,"team":"blue","direction":5},
    {"type":"aircraft","name":"chopper","x":20,"y":12,"team":"blue","direction":2},
    {"type":"aircraft","name":"wraith","x":23,"y":12,"team":"blue","direction":3},

    {"type":"buildings","name":"ground-turret","x":15,"y":23,"team":"green"},
    {"type":"buildings","name":"ground-turret","x":20,"y":23,"team":"green"},

    {"type":"vehicles","name":"scout-tank","x":16,"y":26,"team":"green","direction":4,"orders":{"type":"sentry"}},
    {"type":"vehicles","name":"heavy-tank","x":18,"y":26,"team":"green","direction":6,"orders":{"type":"sentry"}},
    {"type":"aircraft","name":"chopper","x":20,"y":27,"team":"green","direction":2,"orders":{"type":"hunt"}},
    {"type":"aircraft","name":"wraith","x":22,"y":28,"team":"green","direction":3,"orders":{"type":"hunt"}},

    {"type":"buildings","name":"base","x":19,"y":28,"team":"green"},
    {"type":"buildings","name":"starport","x":15,"y":28,"team":"green","uid":-1},
],

/* Economy Related*/
"cash":{
    "blue":5000,
    "green":5000
},

/* Conditional and Timed Trigger Events */
"triggers":[
/* Timed Events*/
    {"type":"timed","time":1000,
        "action":function(){
            game.sendCommand([−1],{type:"construct-unit",details:{type:"aircraft",name:"wraith",orders:
{"type":"patrol","from":{"x":22,"y":30},"to":{"x":15,"y":21}}}});
        }
    },
    {"type":"timed","time":5000,
        "action":function(){
            game.sendCommand([−1],{type:"construct-unit", details:{type:"aircraft",name:"chopper",
orders:{"type":"patrol","from":{"x":15,"y":30},"to":{"x":22,"y":21}}}});
        }
    },
    {"type":"timed","time":10000,
        "action":function(){
            game.sendCommand([−1],{type:"construct-unit",details:{type:"vehicles",name:"heavy-tank",
orders:{"type":"patrol","from":{"x":15,"y":30},"to":{"x":22,"y":21}}}});
        }
    },
    {"type":"timed","time":15000,
        "action":function(){
            game.sendCommand([−1],{type:"construct-unit",details:{type:"vehicles",name:"scout-tank",
orders:{"type":"patrol","from":{"x":22,"y":30},"to":{"x":15,"y":21}}}});
        }
    },
    {"type":"timed","time":60000,
        "action":function(){
            game.showMessage("AI","Now every enemy unit is going to attack you in a wave.");
            var units = [];
            for (var i=0; i < game.items.length; i++) {
                var item = game.items[i];
                if (item.team == "green" && (item.type == "vehicles"|| item.type == "aircraft")){
                    units.push(item.uid);
                }
            };
            game.sendCommand(units,{type:"hunt"});
        }
    },
],

我们做的第一件事是在游戏开始时命令敌人的直升机和幽灵去打猎。接下来,我们分配一个 UID 1 给敌人的星港,并设置一些定时触发器来每隔几秒建造不同类型的巡逻单位。

最后,60 秒后,我们命令所有敌方单位进行狩猎,并使用 showMessage()方法通知玩家。

如果我们现在运行代码,我们可以预期人工智能会很好地保护自己,并在 60 秒结束时非常积极地攻击,如图 9-4 所示。

9781430247104_Fig09-04.jpg

图 9-4。电脑 AI 大举进攻玩家

显然,这是一个相当做作的例子。没有人会想玩一个游戏,在玩的第一分钟就被如此残忍地攻击。然而,正如这个例子所说明的,我们可以通过调整这些触发器和顺序来使游戏变得简单或具有挑战性。

image 提示你可以根据难度设置实现不同的触发器和起始物品,这样玩家就可以根据所选的设置玩同一战役的简单版本或挑战版本。

现在我们已经实现了战斗系统,并探索了使游戏 AI 具有挑战性的方法,我们在这一章最后要看的是加入战争迷雾。

增加了战争的迷雾

战争之雾通常是一层黑色的裹尸布,覆盖了地图上所有未被探索的区域。当玩家单位在地图上移动时,单位可以看到的任何地方的雾都会被清除。

这为游戏引入了探索和阴谋的元素。隐藏在雾中的能力允许使用诸如隐藏基地、伏击和偷袭等策略。

有些 RTS 游戏会在探索某个区域后永久清除雾气,而其他游戏则只会清除玩家单位视线范围内的雾气,并在玩家单位离开该区域后将雾气带回来。对于我们的游戏,我们将使用第二个实现。

定义雾对象

我们将从在 fog.js 中定义一个新的 fog 对象开始,如清单 9-14 所示。

清单 9-14。 实现雾对象(fog.js)

var fog = {
    grid:[],
    canvas:document.createElement('canvas'),
    initLevel:function(){
        // Set fog canvas to the size of the map
        this.canvas.width = game.currentLevel.mapGridWidth*game.gridSize;
        this.canvas.height = game.currentLevel.mapGridHeight*game.gridSize;

        this.context = this.canvas.getContext('2d');

        // Set the fog grid for the player to array with all values set to 1
        this.defaultFogGrid = [];
        for (var i=0; i < game.currentLevel.mapGridHeight; i++) {
           this.defaultFogGrid[i] = [];
           for (var j=0; j < game.currentLevel.mapGridWidth; j++) {
               this.defaultFogGrid[i][j] = 1;
           };
        };

    },
    isPointOverFog:function(x,y){
        // If the point is outside the map bounds consider it fogged
        if(y<0 || y/game.gridSize >= game.currentLevel.mapGridHeight || x<0 || x/game.gridSize >= game.currentLevel.mapGridWidth ){
            return true;
           }
        // If not, return value based on the player's fog grid
        return this.grid[game.team][Math.floor(y/game.gridSize)][Math.floor(x/game.gridSize)] == 1;
    },
    animate:function(){
        // Fill fog with semi solid black color over the map
        this.context.drawImage(game.currentMapImage,0,0)
        this.context.fillStyle = 'rgba(0,0,0,0.8)';
        this.context.fillRect(0,0,this.canvas.width,this.canvas.height);

        // Initialize the players fog grid
        this.grid[game.team] = $.extend(true,[],this.defaultFogGrid);

        // Clear all areas of the fog where a player item has vision
        fog.context.globalCompositeOperation = "destination-out";
        for (var i = game.items.length - 1; i >= 0; i--){
            var item = game.items[i];
            var team = game.team;
                if (item.team == team && !item.keepFogged){
                    var x = Math.floor(item.x );
                    var y = Math.floor(item.y );
                      var x0 = Math.max(0,x-item.sight+1);
                      var y0 = Math.max(0,y-item.sight+1);
                      var x1 = Math.min(game.currentLevel.mapGridWidth-1, x+item.sight-1+ (item.type=="buildings"?item.baseWidth/game.gridSize:0));
                      var y1 = Math.min(game.currentLevel.mapGridHeight-1, y+item.sight-1+ (item.type=="buildings"?item.baseHeight/game.gridSize:0));
                    for (var j=x0; j <= x1; j++) {
                        for (var k=y0; k <= y1; k++) {
                            if ((j>x0 && j<x1) || (k>y0 && k<y1)){
                                if(this.grid[team][k][j]){
                                    this.context.fillStyle = 'rgba(100,0,0,0.9)';
                                    this.context.beginPath();
                                    this.context.arc(j*game.gridSize+12, k*game.gridSize+12, 16, 0, 2*Math.PI, false);
                                    this.context.fill();
                                    this.context.fillStyle = 'rgba(100,0,0,0.7)';
                                    this.context.beginPath();
                                    this.context.arc(j*game.gridSize+12, k*game.gridSize+12,18, 0, 2*Math.PI, false);
                                    this.context.fill();

                                    this.context.fillStyle = 'rgba(100,0,0,0.5)';
                                    this.context.beginPath();
                                    this.context.arc(j*game.gridSize+12, k*game.gridSize+12, 24, 0, 2*Math.PI, false);
                                    this.context.fill();

                                }
                                this.grid[team][k][j] = 0;
                          }
                     };
                 };
             }
        };
        fog.context.globalCompositeOperation = "source-over";
    },
    draw:function(){
        game.foregroundContext.drawImage(this.canvas,game.offsetX, game.offsetY, game.canvasWidth, game.canvasHeight, 0,0,game.canvasWidth,game.canvasHeight);
    }
}

我们首先在雾对象中定义一个画布。initLevel()方法将 canvas 对象的大小调整为当前地图的大小,并定义一个 fogGrid 数组,该数组与所有元素都设置为 1 的地图具有相同的维度。

在 animate()方法中,我们首先用半透明的黑色图层将雾初始化为地图背景。这样,地图上模糊的区域会显示为变暗的背景地形。

然后,我们遍历游戏中的每个物品,并根据玩家物品的视线属性清除其周围的雾数组和雾画布。我们不会为敌对玩家的物品或 keepFogged 属性设置为 true 的物品清除迷雾。

最后,draw()方法使用我们在 game.backgroundContext 上下文上绘制地图时使用的相同偏移量,在 game.foregroundContext 上下文上绘制雾画布。

拨开迷雾

现在我们已经定义了 fog 对象,我们将开始在 index.html 的 head 部分添加一个对 fog.js 的引用,如清单 9-15 所示。

清单 9-15。 【添加引用到雾对象(index.html)

<script src="js/fog.js" type="text/javascript" charset="utf-8"></script>

接下来,我们需要在关卡加载后初始化雾。我们将通过调用 singleplayer 对象的 play()方法中的 fog.initLevel()方法来实现这一点,如清单 9-16 所示。

清单 9-16。 为关卡初始化雾对象(singleplayer.js)

play:function(){
    fog.initLevel();
    game.animationLoop();
    game.animationInterval = setInterval(game.animationLoop,game.animationTimeout);
    game.start();
},

接下来我们需要修改游戏对象的 animationLoop()和 drawingLoop()方法,分别调用 fog.animate()和 fog.draw(),如清单 9-17 所示。

清单 9-17。 调用 fog.animate()和 fog.draw() (game.js)

animationLoop:function(){
    // Animate the Sidebar
    sidebar.animate();

    // Process orders for any item that handles it
    for (var i = game.items.length - 1; i >= 0; i--){
        if(game.items[i].processOrders){
            game.items[i].processOrders();
        }
    };

    // Animate each of the elements within the game
    for (var i = game.items.length - 1; i >= 0; i--){
        game.items[i].animate();
    };

    // Sort game items into a sortedItems array based on their x,y coordinates
    game.sortedItems = $.extend([],game.items);
    game.sortedItems.sort(function(a,b){
        return b.y-a.y + ((b.y==a.y)?(a.x-b.x):0);
    });

    fog.animate();

    game.lastAnimationTime = (new Date()).getTime();
},
drawingLoop:function(){
    // Handle Panning the Map
    game.handlePanning();

    // Check the time since the game was animated and calculate a linear interpolation factor (−1 to 0)
    // since drawing will happen more often than animation
    game.lastDrawTime = (new Date()).getTime();
       if (game.lastAnimationTime){
           game.drawingInterpolationFactor = (game.lastDrawTime-game.lastAnimationTime)/game.animationTimeout - 1;
           if (game.drawingInterpolationFactor>0){ // No point interpolating beyond the next animation loop...
               game.drawingInterpolationFactor = 0;
           }
       } else {
        game.drawingInterpolationFactor = −1;

    }

    // Since drawing the background map is a fairly large operation,
    // we only redraw the background if it changes (due to panning)
    if (game.refreshBackground){
        game.backgroundContext.drawImage(game.currentMapImage,game.offsetX, game.offsetY, game.canvasWidth, game.canvasHeight, 0,0,game.canvasWidth,game.canvasHeight);
        game.refreshBackground = false;
    }

    // Clear the foreground canvas
    game.foregroundContext.clearRect(0,0,game.canvasWidth,game.canvasHeight);

    // Start drawing the foreground elements
    for (var i = game.sortedItems.length - 1; i >= 0; i--){
        if (game.sortedItems[i].type != "bullets"){
            game.sortedItems[i].draw();
        }
    };

    // Draw the bullets on top of all the other elements
    for (var i = game.bullets.length - 1; i >= 0; i--){
        game.bullets[i].draw();
    };

    fog.draw();

    // Draw the mouse
    mouse.draw()

    // Call the drawing loop for the next frame using request animation frame
    if (game.running){
        requestAnimationFrame(game.drawingLoop);
    }
},

如果我们现在运行代码,我们应该会看到整个地图笼罩在战争的迷雾中,如图图 9-5 所示。

9781430247104_Fig09-05.jpg

图 9-5。笼罩在战争迷雾中的地图

你会看到友方单位和建筑周围的雾气被揭开了。此外,雾区显示原始地形,但不显示其下的任何单位。

当我们不知道对方军队的规模和位置时,同样的敌人攻击会令人感到更加恐怖。在我们结束这一章之前,我们将做一些补充,从使模糊的区域不可建造开始。

使得有雾的区域不可建造

我们要做的第一个改变是通过使雾区不可建造来防止在雾区部署建筑。我们将修改侧边栏对象的 animate()方法,如清单 9-18 所示。

清单 9-18。 使模糊区域不可构建(sidebar.js)

animate:function(){
    // Display the current cash balance value
    $('#cash').html(game.cash[game.team]);

    //  Enable or disable buttons as appropriate
    this.enableSidebarButtons();

    if (game.deployBuilding){
        // Create the buildable grid to see where building can be placed
        game.rebuildBuildableGrid();
        // Compare with buildable grid to see where we need to place the building
        var placementGrid = buildings.list[game.deployBuilding].buildableGrid;
        game.placementGrid = $.extend(true,[],placementGrid);
        game.canDeployBuilding = true;
        for (var i = game.placementGrid.length - 1; i >= 0; i--){
            for (var j = game.placementGrid[i].length - 1; j >= 0; j--){
                if(game.placementGrid[i][j] &&
                    (mouse.gridY+i>= game.currentLevel.mapGridHeight || mouse.gridX+j>= game.currentLevel.mapGridWidth
                        || game.currentMapBuildableGrid[mouse.gridY+i][mouse.gridX+j]==1 || fog.grid[game.team][mouse.gridY+i][mouse.gridX+j]==1 )){
                    game.canDeployBuilding = false;
                    game.placementGrid[i][j] = 0;
                }
            };
        };
    }
},

在创建 placementGrid 数组时,我们添加了一个额外的条件来测试雾化网格,这样就不能再构建雾化的网格方块了。如果我们运行游戏并试图在一个有雾的区域建造,我们应该会看到一个警告,如图 9-6 所示。

9781430247104_Fig09-06.jpg

图 9-6。不能在有雾的地区部署建筑

正如你所看到的,建筑部署网格在有雾的区域变成红色,表示玩家不能在那里建筑。如果你仍然试图点击一个模糊的区域,你会得到一个系统警告。

接下来,我们将确保玩家不能选择或探测到在雾中的建筑或单位。我们通过修改鼠标对象的 pointUnderFog()方法来做到这一点,如清单 9-19 所示。

清单 9-19。 【雾下藏物】(mouse.js)

itemUnderMouse:function(){
    if(fog.isPointOverFog(mouse.gameX,mouse.gameY)){
        return;
    }
    for (var i = game.items.length - 1; i >= 0; i--){
        var item = game.items[i];
        if (item.type=="buildings" || item.type=="terrain"){
            if(item.lifeCode != "dead"
                && item.x<= (mouse.gameX)/game.gridSize
                && item.x >= (mouse.gameX - item.baseWidth)/game.gridSize
                && item.y<= mouse.gameY/game.gridSize
                && item.y >= (mouse.gameY - item.baseHeight)/game.gridSize
                ){
                    return item;
            }
        } else if (item.type=="aircraft"){
            if (item.lifeCode != "dead" &&
                Math.pow(item.x-mouse.gameX/game.gridSize,2)+Math.pow(item.y-(mouse.gameY+item.pixelShadowHeight)/game.gridSize,2) < Math.pow((item.radius)/game.gridSize,2)){
                return item;
            }
       }else {
            if (item.lifeCode != "dead" && Math.pow(item.x-mouse.gameX/game.gridSize,2) + Math.pow(item.y-mouse.gameY/game.gridSize,2) < Math.pow((item.radius)/game.gridSize,2)){
                return item;
            }
        }
    }
},

我们检查鼠标下的点是否模糊,如果是,不返回任何东西。随着这最后的改变,我们现在在游戏中有一个工作的战争迷雾。

摘要

在这一章中,我们为我们的游戏实现了一个战斗系统。我们从用不同类型的项目符号定义一个项目符号对象开始。然后,我们在炮塔、飞机和车辆上添加了几个基于战斗的订单状态。我们使用这些命令和上一章定义的触发系统来创造一个相当有挑战性的敌人。最后,我们实现了战争迷雾。

我们的游戏现在拥有了 RTS 的大部分基本元素。在下一章,我们将通过添加声音来完善我们的游戏框架。然后,我们将使用这个框架来构建一些有趣的关卡,并结束我们的单人战役。

转载请注明出处或者链接地址:https://www.qianduange.cn//article/19049.html
标签
评论
发布的文章
大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!