精华内容
下载资源
问答
  • 极客江南: 一个开发技术特别执着的程序员,对移动开发有着独到的见解和深入的研究,有着多年的iOS、Android、HTML5开发经验,对NativeApp、HybridApp、...开发一个游戏,首先你需要知道游戏的规则。 这个游戏名为狂.

    极客江南: 一个对开发技术特别执着的程序员,对移动开发有着独到的见解和深入的研究,有着多年的iOS、Android、HTML5开发经验,对NativeApp、HybridApp、WebApp开发有着独到的见解和深入的研究, 除此之外还精通 JavaScript、AngularJS、 NodeJS 、Ajax、jQuery、Cordova、React Native等多种Web前端技术及Java、PHP等服务端技术。

    先上一张效果图:
    在这里插入图片描述

    • 开发思路

    开发一个游戏,首先你需要知道游戏的规则。

    这个游戏名为狂拍灰太狼。

    规则:

    • 游戏时间 60 s
    • 游戏角色为灰太狼、小灰灰
    • 拼手速殴打灰太狼
    • 殴打灰太狼 + 10 分,殴打小灰灰 - 10 分

    开发技术

    • html
    • css
    • jq

    实现思路

    • 1.利用 html + css 布局游戏界面
    • 2.导入 jq 库
    • 3.实现狂拍灰太狼游戏逻辑

    核心逻辑

    • 封装 60 s 进度条方法
    • 封装处理灰太狼动画的方法
    • 游戏按钮点击监听

    HTML 代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>狂拍灰太狼</title>
        <link rel="stylesheet" href="css/index.css">
        <script src="js/jquery-1.12.4.js"></script>
        <script src="js/index.js"></script>
    </head>
    <body>
    <div class="container">
        <h1 class="score">0</h1>
        <div class="progress"></div>
        <button class="start">开始游戏</button>
        <div class="rules">游戏规则</div>
        <div class="rule">
            <p>游戏规则:</p>
            <p>1.游戏时间:60s</p>
            <p>2.拼手速,殴打灰太狼+10分</p>
            <p>3.殴打小灰灰-10分</p>
            <a href="#" class="close">[关闭]</a>
        </div>
        <div class="mask">
            <h1>GAME OVER</h1>
            <button class="reStart">重新开始</button>
        </div>
    </div>
    </body>
    </html>
    

    css 代码

    *{
        margin: 0;
        padding: 0;
    }
    .container{
        width: 320px;
        height: 480px;
        background: url("../images/game_bg.jpg") no-repeat 0 0;
        margin: 50px auto;
        position: relative;
    }
    .container>h1{
        color: white;
        margin-left: 60px;
    }
    .container>.progress{
        width: 180px;
        height: 16px;
        background: url("../images/progress.png") no-repeat 0 0;
        position: absolute;
        top: 66px;
        left: 63px;
    }
    .container>.start{
        width: 150px;
        line-height: 35px;
        text-align: center;
        color: white;
        background: linear-gradient(#E55C3D,#C50000);
        border-radius: 20px;
        border: none;
        font-size: 20px;
        position: absolute;
        top: 320px;
        left: 50%;
        margin-left: -75px;
    }
    .container>.rules{
        width: 100%;
        height: 20px;
        background: #ccc;
        position: absolute;
        left: 0;
        bottom: 0;
        text-align: center;
    }
    .container>.rule{
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,0.5);
        position: absolute;
        left: 0;
        top: 0;
        padding-top: 100px;
        box-sizing: border-box;
        text-align: center;
        display: none;
    }
    .rule>p{
        line-height: 50px;
        color: white;
    }
    .rule>a{
        color: red;
    }
    .container>.mask{
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,0.5);
        position: absolute;
        left: 0;
        top: 0;
        padding-top: 200px;
        box-sizing: border-box;
        text-align: center;
        display: none;
    }
    .mask>h1{
        color: #ff4500;
        text-shadow: 3px 3px 0 #fff;
        font-size: 40px;
    }
    .mask>button{
        width: 150px;
        line-height: 35px;
        text-align: center;
        color: white;
        background: linear-gradient(#74ACCF,#007DDC);
        border-radius: 20px;
        border: none;
        font-size: 20px;
        position: absolute;
        top: 320px;
        left: 50%;
        margin-left: -75px;
    }
    

    jq 代码

    $(function () {
        // 1.监听游戏规则的点击
        $(".rules").click(function () {
            $(".rule").stop().fadeIn(100);
        });
    
        // 2.监听关闭按钮的点击
        $(".close").click(function () {
            $(".rule").stop().fadeOut(100);
        });
    
        // 3.监听开始游戏按钮的点击
        $(".start").click(function () {
            $(this).stop().fadeOut(100);
            // 调用处理进度条的方法
            progressHandler();
            // 调用处理灰太狼动画的方法
            startWolfAnimation();
        });
    
        // 4.监听重新开始按钮的点击
        $(".reStart").click(function () {
            $(".mask").stop().fadeOut(100);
            // 调用处理进度条的方法
            progressHandler();
            // 调用处理灰太狼动画的方法
            startWolfAnimation();
        });
    
        // 定义一个专门处理进度条的方法
        function progressHandler() {
            // 重新设置进度条的宽度
            $(".progress").css({
                width: 180
            });
            // 开启定时器处理进度条
            var timer = setInterval(function () {
                // 拿到进度条当前的宽度
                var progressWidth = $(".progress").width();
                // 减少当前的宽度
                progressWidth -= 1;
                // 重新给进度条赋值宽度
                $(".progress").css({
                    width: progressWidth
                });
                // 监听进度条是否走完
                if(progressWidth <= 0){
                    // 关闭定时器
                    clearInterval(timer);
                    // 显示重新开始界面
                    $(".mask").stop().fadeIn(100);
                    // 停止灰太狼的动画
                    stopWolfAnimation();
                }
            }, 100);
        }
    
        var wolfTimer;
        // 定义一个专门处理灰太狼动画的方法
        function startWolfAnimation() {
            // 1.定义两个数组保存所有灰太狼和小灰灰的图片
            var wolf_1=['./images/h0.png','./images/h1.png','./images/h2.png','./images/h3.png','./images/h4.png','./images/h5.png','./images/h6.png','./images/h7.png','./images/h8.png','./images/h9.png'];
            var wolf_2=['./images/x0.png','./images/x1.png','./images/x2.png','./images/x3.png','./images/x4.png','./images/x5.png','./images/x6.png','./images/x7.png','./images/x8.png','./images/x9.png'];
            // 2.定义一个数组保存所有可能出现的位置
            var arrPos = [
                {left:"100px",top:"115px"},
                {left:"20px",top:"160px"},
                {left:"190px",top:"142px"},
                {left:"105px",top:"193px"},
                {left:"19px",top:"221px"},
                {left:"202px",top:"212px"},
                {left:"120px",top:"275px"},
                {left:"30px",top:"295px"},
                {left:"209px",top:"297px"}
            ];
    
            // 3.创建一个图片
            var $wolfImage = $("<images src='' class='wolfImage'>");
            // 随机获取图片的位置
            var posIndex = Math.round(Math.random() * 8);
            // 4.设置图片显示的位置
            $wolfImage.css({
               position: "absolute",
                left:arrPos[posIndex].left,
                top:arrPos[posIndex].top
            });
            // 随机获取数组类型
            var wolfType = Math.round(Math.random()) == 0 ? wolf_1 : wolf_2;
            // 5.设置图片的内容
            window.wolfIndex = 0;
            window.wolfIndexEnd = 5;
            wolfTimer = setInterval(function () {
                if(wolfIndex > wolfIndexEnd){
                    $wolfImage.remove();
                    clearInterval(wolfTimer);
                    startWolfAnimation();
                }
                $wolfImage.attr("src", wolfType[wolfIndex]);
                wolfIndex++;
            }, 300);
    
            // 6.将图片添加到界面上
            $(".container").append($wolfImage);
    
            // 7.调用处理游戏规则的方法
            gameRules($wolfImage);
        }
    
        function gameRules($wolfImage) {
            $wolfImage.one("click",function () {
                // 修改索引
                window.wolfIndex = 5;
                window.wolfIndexEnd = 9;
    
                // 拿到当前点击图片的地址
                var $src = $(this).attr("src");
                // 根据图片地址判断是否是灰太狼
                var flag = $src.indexOf("h") >= 0;
                // 根据点击的图片类型增减分数
                if(flag){
                    // +10
                    $(".score").text(parseInt($(".score").text()) + 10);
                }else{
                    // -10
                    $(".score").text(parseInt($(".score").text()) - 10);
                }
            });
        }
        function stopWolfAnimation() {
            $(".wolfImage").remove();
            clearInterval(wolfTimer);
        }
    });
    

    最终效果
    在这里插入图片描述
    在这里插入图片描述

    基本制作过程不是很难,核心是理解其中的业务逻辑。

    大家记得收藏前,先点个赞哦!好的文章值得被更多人看到。

    推荐阅读:

    13万字C语言保姆级教程2021版

    10万字Go语言保姆级教程

    2 万字 Jquery 入门教程

    3 万字 html +css 入门教程

    最后,再多一句,粉丝顺口溜,关注江哥不迷路,带你编程上高速。

    展开全文
  • 游戏开发入门(游戏开发概述

    万次阅读 多人点赞 2018-01-17 21:55:54
    视频链接:游戏开发入门(游戏开发概述(3节课 时常:约1小时03分钟) 该视频围绕电子游戏的发展与开发简单介绍了一些基本常识,同时提出并解决了几问题 笔记与总结(请先学习视频内容): 1.游戏机发展...

    视频链接:游戏开发入门(一)游戏开发概述(3节课 时常:约1小时03分钟)
    该视频围绕电子游戏的发展与开发简单介绍了一些基本常识,同时提出并解决了几个问题


    笔记与总结(请先学习视频内容):

    1.游戏机发展

    • 20世纪70年代开始 首款主机magnavox odysse
    • 80年代 红白机
    • 80-90年代 各类非FC主机(PS,NDS等) PC单机游戏
    • 90年代-2000年 局域网对战游戏
    • 2000年后 大型多人游戏 页游 手游
    • 现在 VR,AR ..

    2.游戏是如何开发出来的,开发流程是什么?
    原始的游戏比较粗糙,内容也很少,一般就一个人开发,美术与策划都由自己完成(建议看看《doom启示录》)。
    随着游戏逐渐变得复杂,需要多人去合作完成,分工也变得明显。简单的流程如下:

    • a.首先讨论确定游戏的基本内容,类型等,对游戏项目立案
    • b.程序员构建最基本的游戏框架,单机还是网游,什么平台(在当前,某些非常简单的游戏demo也可以跳过编程,直接使用游戏引擎)
    • c.策划对游戏细节进行设计与构建(包括剧情,玩法,关卡,数值,系统等)
    • d.美术需要根据当前策划设想的游戏世界,提供合适的艺术效果(包括原画,模型,特效,UI,动画,场景等)
    • e.程序,美术,策划三方不断的相互沟通完善游戏内容并不断的测试调优(这是最为耗时的一个流程) 注意:这里策划不仅仅是思考游戏的玩法,还需要将程序员留出来的接口做各种配置,因为项目的需求变化过于频繁
    • f.游戏内容基本完成后,需要质检等人员开始测试,程序员修复Bug
    • g.游戏经过一定测试后,发布。同时交由运维人员做基本的维护,发布前期一定有bug,还需要各个职能人员互相配合

    2.开发游戏需要哪些技术?

    • a.作为一个程序员需要的基本素质:包括掌握基本语言编程,深入了解语言特性,了解计算机相关基础,设计模式等 其实就这一块很多人就很难达标,不过我们可以在做游戏的同时提升自己的功底
    • b.构建一个游戏需要了解不同平台的相关技术,但一般不需要太深入
    • c.需要有一定的引擎知识,尽可能熟悉引擎各个功能的使用
    • d.常见的脚本技术(lua等)
    • e.随着3D游戏的发展,需要对图形图像知识有一定深度的理解(涉及到数学,非常复杂与庞大的模块)
    • f.网络游戏需要掌握基本网络基础知识(TCP/IP,Socket,Http),了解同步机制
    • g.动画技术
    • h.了解会使用物理引擎
    • i.基本的场景管理技术
    • j. AI相关技术,游戏中大部分的AI还是比较简单的,不需要机器学习

    3.程序员一般负责的内容是什么?

    • a.实现策划需求(大部分是逻辑需求)
    • b.提供内部工具,加快开发。比如当前引擎不支持某种NPC对象的高亮显示,无法拖拽资源到游戏里面等功能,需要程序员提供 提高开发效率
    • c.修复Bug
    • e.优化体验,也就是常说的性能优化,提高帧率,减小内存开销等等

    目前规模较大的公司,程序员也分为多种(一般分为逻辑程序员和引擎程序员),所完成的内容也有差别。
    逻辑程序一般属于频繁与策划沟通,直接对项目需求进行开发的程序
    引擎程序一般是研究引擎功能,优化引擎(提示表现,提高性能等),不过这也并不是绝对的。

    4.如何入行游戏(建议)

    • a.夯实基础(语言,数据结构,算法,网络,操作系统,图形学等)
    • b.对游戏开发有一个基本的认识,建议可以先根据我的文章讲解看一遍这个系列的视频(遇到不懂的名词就去查,就去研究), 然后通读一遍游戏引擎架构
    • c.实践!选择一个游戏引擎然后动手开发游戏demo,参考其他游戏demo,尽可能的应用学过的游戏知识。多思考,多理解
    • d.想办法进入业界,从项目中吸取知识。如果有机会的话(应届生机会比较多),只要程序基础够也是可以加入该行业的

    5.新手与真正开发者的差距有哪些?
    最多的就是经验了,前面提到的技术都是概括性的,每个方面深入进去都是无限的坑。新手一般只会用而不懂为什么这么用, 真正有经验的开发者不仅会用,甚至可以模仿出来一套几乎相同的框架并完善,遇到问题也有更多的解决思路与方案。


    上一篇:游戏开发入门系列(目录)
    下一篇:游戏开发入门教程(二):游戏中的设计模式

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78797928

    展开全文
  • 如何开发一游戏游戏开发流程及所需工具

    万次阅读 多人点赞 2018-01-03 00:00:00
    「文末高能」编辑 | 哈比游戏作为娱乐生活的一个方面,参与其中的越来越多,而大部分参与其中的都是以玩家的身份。他们热爱一款游戏,或是被游戏的故事情节、炫丽的场景、动听的音乐所艳羡,亦或是被游戏中角色...

    本文来自作者 goto先生  GitChat 上分享 「如何开发一款游戏:游戏开发流程及所需工具」

    编辑 | 哈比

    游戏作为娱乐生活的一个方面,参与其中的人越来越多,而大部分参与其中的人都是以玩家的身份。

    他们热爱一款游戏,或是被游戏的故事情节、炫丽的场景、动听的音乐所艳羡,亦或是被游戏中角色扮演、炫酷的技能、有趣的任务所吸引,然而他们中的大多数可能并不了解如此一款好玩的游戏是如何打造出来的。

    对于想来这个行业尝试的新人们,先对游戏开发制作有个整体的了解也是非常必要的。

    接下来我将从几个方面来分别进行阐述。

    基础知识

    游戏,说白了就是一个程序,这个程序或在 PC 上或在移动设备上运行,玩家通过与这个程序交互来达到娱乐性的目的。我们先了解一下游戏中用到的各种引擎以及游戏相关术语。

    游戏引擎

    游戏引擎是游戏研发的主程序接口,它为开发者提供了各种开发游戏的的工具,即可编辑游戏系统和实时图像系统的核心组件,其目的就在于让开发者可以快速的做出游戏而不必从零开始。

    游戏引擎包含渲染引擎、物理引擎、碰撞检测系统、网络引擎、音效引擎、脚本引擎、动画及场景管理等。

    • 渲染引擎:是对游戏中的对象和场景起到渲染的效果,游戏中的角色都是通过渲染引擎将它的模型、动画、光影、特效等所有效果实时计算出来并展示到屏幕;

    • 物理引擎:让对象运动遵循特定的规律,比如当角色跳起的时候,系统内定的重力值将决定它弹跳的高度及下落的速率;

    • 碰撞检测系统:可以探测各物体的边缘,当两个 3D 物体在一起的时候,系统可以防止它们相互穿过;

    • 网络引擎:是负责玩家与设备间的通信,处理来自键盘、鼠标及其它外设信号。若游戏联网,它也用来管理客户端与服务器间的通信;

    • Lua 引擎:是 Lua 的服务器引擎,lua 是一种轻量级的嵌入式脚本语言,在网游开发中应用广泛。

    总的来说,一个游戏是引擎和资源组成的,资源包括图象、声音、动画等,游戏引擎就像一个发动机,控制着游戏的运行,它按游戏设计规则依次调用游戏资源。

    游戏名词

    • CD-key:游戏的序列号或防盗密码;

    • BugFree:测试管理平台,是一款基于 Web 的开源错误追踪工具;

    • Ping:从客户端发送数据到服务器到接收到服务器反馈数据的时间,以 ms 计,若 Ping 值高会感觉延迟;

    • Proxy Server:代理服务器,代理网络用户去取得网络信息;

    • PU:付费用户;

    • RU:注册用户;

    • AU:活跃用户;

    • DAU:平均每日活跃用户;

    • CCU:同时在线人数;

    • PCU:最高同时在线人数;

    • ACU:平均同时在线人数;

    • ARPPU:付费玩家平均收入;

    • 封测:限定用户数量的游戏测试,用来对技术和游戏产品进行初步的验证,用户规模较小;

    • 内测:面向一定数量用户进行的内部游戏测试,多用于检测游戏压力和功能有无漏洞;

    • 公测:对所有用户公开的开放性的网络游戏测试。

    游戏的种类

    游戏的分类方法很多,可以按终端、内容、摄像类型、玩家格斗对象、玩家人数等来分,其中按内容来分最直观,它可以根据游戏的元素迅速锚定游戏范围。

    • 按终端分:主机游戏 (电视机游戏)、客户端游戏、网页游戏、手机游戏;

    • 按摄影类型分:2D 游戏、2.5 游戏、3D 游戏;

    • 按格斗对象分:PVE:PlayerVsEnvironment、PVP:PlayerVsPlayer;

    • 按玩家人数分:单机游戏(Singe-Player Game)、多人游戏(Muti-Player Game)、大型多人在线(Massive Multiplayer Online Game)。

       

    我们可以看一下 AppStore 中游戏的分类,非常鲜明,如下图所示。

    游戏的开发流程

    游戏开发从狭义上讲就是程序部门进行相关游戏程序的编写,从广义上讲,是整个游戏制作过程,这其中包括多个部门的人员配备。下图是一个一般性的游戏开发团队。

    整个团队包含四个部门,即策划、美术、程序、制作人,各个部门负责不同的工作,协调完成整个游戏的开发。

    策划是团队的灵魂,也分执行策划、数据策划、表现策划、资源策划等,他们主要对游戏剧情、背景进行分析设计,对游戏中的各种规则进行描述及公式确定,对各种资料表格进行维护,对游戏中的特效、动作等进行收集并提出需求,进行 UI 设计及模型相关配置等。

    程序是团队的骨肉,也可细分为主程序、客户端引擎、服务器引擎、3D 程序、AI 程序、脚本程序、数据库程序等,他们主要负责确定程序的数据结构,确定策划方案的完成方法,将策划提出的各种需求用程序来实现,并为游戏开发过程提供良好的编辑工具。

    美术是团队的皮肤,可细分为人物原画、人物建模、材质贴图、人物动作、场景动画等,他们主要负责整个游戏的视觉风格,以及人物模型动作等的设计等。

    制作人主要进行游戏的外部统筹,市场调研、游戏开发进度、游戏版权、游戏宣传、游戏发布及音乐音效素材的管理都是制作人工作的范畴。

    下图是某国外游戏研发团队的组织架构图,可以参考了解一下。

    游戏开发的各个时期

    对于游戏制作人来说,每个游戏从产生要消亡要经历各个阶段,下面是普遍适用的典型范例,但并不是每个游戏都要经历所有的时期。

    • 概念时期:就是整个游戏概念的确定,要做什么样的游戏,主题线索是什么;

    • 原型开发时期:这个时期要制作游戏的原型,用来体验游戏的设计概念,从而纠正和改善不足的地方;

    • 推广时期:此时是游戏开发方向出版方推广产品,向投资方展示游戏的设计概念、主要卖点、产品如何适应市场的需求、产品开发的可行性及具体的实现方案;

    • 准备时期:这个时期主要处理游戏项目所涉及的商务及法律方面的事务,比如游戏专利、剧本版权、品牌商标等,从而组织开发团队制作大致的方案,确定游戏开发所需要的工具及其它细节问题;

    • 制作时期: 这个时期是游戏制作的主体时期,完成 3D 模型的制作,场景制作,过场动画、画面渲染及音效录制等,游戏引擎和资源在此时期将被完全整合到一起。

    • 质量保证时期:这个时期是游戏的 QA 或测试时期,主要用来保证游戏的各项功能是否完好,从而发现和修复各种 Bug 和错误;

    • 母盘生成时期:这个时期是将游戏存盘交由平台厂商测试检测的时期,每个平台厂商的测试标准不尽相同,这个时期中也需要不断地测试改进游戏,修复 Bug,准备市场投放。

    • 运营维护时期:这个时期是游戏发布后持续运营,在运营过程中发现问题,修复并更新升级的过程,这是一个长期的过程。

    项目流程

    一部游戏完整的开发过程,归纳起来可分为五步,如下图所示。

    市场调研可以分为三个小部分,

    1)调研前进行 “头脑风暴”,让尽量多的人想出尽量多的创意点子并做好记录,从而在市场调研过程中一一确认,不符合的排除;

    2)撰写策划草案,从而让项目小组中的每一个成员对开发的项目有一个大体的认识,并且对目标明确;

    3)对每一个草案都进行市场调研和分析,决定是否要开发这个游戏。市场调研主要从两个方面入手,即目标客户(玩家)和开发成本。

    需求分析主要是撰写需求分析书,这主要包括三个方面:

    1)策划需求

    • 策划的分工:包括剧本、数值、界面、执行等方面;

    • 进度控制:要时刻注意时间和开发进度的控制,需要写一个专门的项目进度汇总表。

    2)美术需求

    • 场景:包括游戏地图、小场景等方面;

    • 人物:包括玩家角色、重要 NPC(玩家队友、提供任务的 NPC、主线剧情 NPC 等)、次要 NPC(路人、村民等)、怪物、BOSS 等;    

    • 动画:动画方面估计每个公司的需求都不尽相同。如果公司能力有限,动画的制作可以考虑外包的方式;

    • 道具:主要需要考虑是否采取纸娃娃系统;

    • 全身像:人物的全身像方面;

    • 静画 &CG:游戏中可能出现的静画和 CG 的需求,没有则不需要写;

    • 人物头像:人物的头像制作需求,其中包括人物的表情方面,包括喜、怒、哀、乐和悲等多种表情;

    • 界面:界面的需求,包括主界面、各项子界面、屏幕界面、开头界面、END 界面、保存和载入界面等方面;

    • 动态物件:包括游戏中可能出现的火把、光影等方面;

    • 卷轴:又称为滚动条。根据游戏的情况来定具体的需求;

    • 招式图:根据游戏开发的具体情况决定是否有此需求;

    • 编辑器图素:各种编辑器的图素需求,例如关卡编辑器、地图编辑器等方面;

    • 粒子特效:3D 粒子特效的需求;

    • 宣传画:包括游戏的宣传画、海报等方面的制作需求;

    • 游戏包装:游戏客户端的封面包装的制作;

    • 说明书插图:游戏说明书内附插图的制作需求;

    • 盘片图鉴:游戏客户端盘片上的图鉴的制作需求;

    • 官方网站:游戏官方网站的制作需求。

    3)程序需求

    • 地图编辑器:包括编辑器的功能需求、各种数据的需求等;

    • 粒子编辑器:关于粒子编辑器的需求;

    • 内镶小游戏:包括游戏内部各种小游戏的需求;

    • 功能函数:包括游戏中可能会出现的各种程序功能、技术参数、数据、碰撞检测、AI 等方面的需求;

    • 系统需求:包括升级系统、道具系统、招式系统等系统导入器的需求。

       

    项目开发步骤就是将整个游戏项目的资源通过引擎组织起来,对游戏的架构、功能及各逻辑模块进行充分的整合。

    这就要明确游戏开发的日程和进度安排,这也是充分利用各种开发工具让开发效率大大提升的根本所在。

    测试发布流程主要包括两次大型正规的测试,即 Alpha 测试和 Beta 测试,其中前者意味着游戏的功能和流程完整,QA 会为游戏定制测试计划,测试人员将发现的 Bug 提交到数据库,开发和设计人员对相应的错误进行修复。

    后者意味着游戏中的各种资源已完成,产品已定型,后期只是修复 Bug。在这两次测试修复后,得到待发布的 Release 版。

    Gold Release 流程主要是开发游戏的各种补丁包、游戏的升级版本,以及官方的各种礼包和插件等。

    游戏开发所用的工具

    选择正确的工具,可以为游戏项目节省开支,提高工作质量,降低项目风险,让整个项目团队成员集中注意力,从而把游戏做得好玩。

    程序工具软件

    • OpenGL ES——OpenGL 长期以来都是行业内 2D/3D 图形高质表现的标准,它适用于各种设备。OpenGL ES 提供了在软件应用程序和软件图像引擎间的底层 API 接口;

    • IncrediBuild——这个开发工具极大的提升了 VS/VC 的编译和版本生成速度,有效降低增量构建所需要花费的时间,它主要是采用分布式编译技术,在公司内网可以调用其它计算机的资源进行快速编译。这是开发人员不可多得的一款好工具;

    • VS2013——微软的 VS 集成开发环境多年来都是游戏制作的基本软件,界面友好,功能齐全,可以极大的提升编码速度和工作流;

    • Visual Assist X——这是一个插件,引入了强大的编辑功能,完全整合在 C++IDE 环境中,可以极大的提升开发人员的工作进程,不过有的 IDE 环境已经整合了这款插件,自己不用手动安装了;

    • Direct X——它是微软在过去建立的众多行业标准之一,它是一种视窗技术,可以让你在玩游戏或观看视频过程中图像和音效有更高的品质,它包含多个配套组件,如 Direct3D、DirectSound、DirectPlay、DirectInput 等。

    美术制作工具

    美术制作工具要远多于程序软件,因此在游戏开发过程中,选择美术软件时要慎重考虑,以方便项目的顺利进展。

    • Maya——它是行业内首选的 3D 动画制作软件之一,它功能十分强大,可用于高端电脑构图,可以处理几乎所有的 3D 制作工作。

      比如模型构建、动画制作、描绘渲染、电影特效等。但其缺点也在于其多边形建模工具不太理想;

    • 3D Studio Max——它是游戏开发中 3D 程序开发的主流引导者,其多边形建模工具是所有 3D 程序中最棒的工具,用它进行开发效率也特别高;

    • PhotoShop CS——该软件在游戏制作中被广泛应用,是游戏制作的必备软件,它在游戏开发的各个时期都会用到,包括前期制作到最终完成并市场推广。美术人员用它来做出游戏环境和角色的设定,策划也用它来画关卡规划和界面示意图;

    • FaceGen Modeller——这是一款 3D 头脸创作工具,它可以为游戏制作多个角色,从而快速做出人物脸部及头部模型,形态非常逼真;

    • Zbrush——这款工具的特点在于使艺术模型呈现传统艺术创作的过程,它可以辅助制作人员做出逼真的环境多边模型,是地图场景的绝佳工具;

    • Granny——可以作为游戏的一个批量输出工具,它能够完成所有艺术素材,包括模型、渲染和过场动画的植入。它可以生成法线和纹理贴图,更是一款引擎解释工具。

    游戏组件工具

    游戏组件是指游戏的基本环境架构,比如描绘、场景和几何构型的构建,也称为中间件。

    • Havok——这是目前比较先进的物理引擎,它能让游戏模拟现实,可以将游戏做出非常逼真的效果;

    • Gamebryo——这是一款能够帮助开发人员快速制作原型版的工具,功能强大,运行稳定,是比较好的 3D 实时图形引擎,其强大的渲染引擎和动作处理系统使其在商业上获得巨大的成功;

    • Quazal——它属于网络建筑中间件,主要用于制作大型多人在线游戏,其它类似的中间件有 Big World。

    音效工具

    音效作为游戏里的重要组成部分,选择合适的工具也非常重要。作为游戏开发人员,关键要了解各种工具的使用限制,有很多的专业音效制作工具,包括 Nuendo、Vegas、Logic、ProTools、Peak、GameCODA、SoundForge 等。

    场景构建工具:

    • Unreal Engine——这是一款比较完型的游戏开发引擎,它提供了比较全能的关卡编辑器、过场动画系统、3D 图形及 AI;

    • Source——这款引擎为人物角色动画提供了新技术,先进的 AI、光影渲染、实景图象都非常棒,引擎也包含了先进的物理引擎。

    日常管理工具

    游戏开发过程中所涉及的事务比较多,内容也比较繁杂,用好日常管理工具可以有效提升工作效率。下面是几个用得比较多的工具:

    • MicroSoft Excel——利用它进行开发进度管理,开发人员可以非常轻松地跟踪管理多个游戏开发部门的进度,开发人员必须要对其十分熟悉,才能用的得心应手;

    • 日常工作增量进程报告 (daily delta reports)——一个项目成功的关键就是运用日常工作进程报告,在这个过程中,每一名团队成员每天上交一份个人当日工作完成情况清单。这种进程报告的方式可以简明扼要、方便有效地跟踪项目进程;

    • 源码控制报告和版本控制报告——目前大部分项目研发用的版本控件工具是 SVN、Perforce、Git 等,在使用版本控制软件前,一定要花一定的时间来熟悉软件的功能和使用方法,这对于游戏研发人员非常关键,否则就会犯些不必要的错误,从而导致工作效率下降;

    • 运用 WiKi——它是协作性文档,是自由讨论和创造性工具,是最佳管理设计性文档的方法,当团队无法建立一个内部局域网来管理各种记录和设计进程或建立局域网工作量过大时,WiKi 就是你最佳的选择。

    好了,关于游戏开发的相关知识,我就介绍到这里。游戏开发涉及的知识太多太多,我在这里只是概括性的做了一个引入,希望对您有些许的帮助,文章内容不免有很多不足之处,还请各位大侠多多指教。

     

    推荐书籍

    《游戏开发 世嘉新人培训教材》

    《游戏设计入门:理解玩家思维》

     

    展开全文
  • Node 开发一个多人对战的射击游戏

    万次阅读 2021-07-21 00:55:58
    大家好,我是漫步,今天分享一个Nodejs实战长文,希望对你有所帮助。……相信大家都是知道游戏的吧。这玩意还是很有意思的,无论是超级玛丽,还是魂斗罗,亦或者是王者荣耀以及阴阳师。当然,这篇...

    大家好,我是漫步,今天分享一个Nodejs实战长文,希望对你有所帮助。

    ……

    相信大家都是知道游戏的吧。

    这玩意还是很有意思的,无论是超级玛丽,还是魂斗罗,亦或者是王者荣耀以及阴阳师。

    当然,这篇文章不涉及到那么牛逼的游戏,这里就简单的做一个小游戏吧。

    先给它取个名字,就叫“球球作战”吧。

    咳咳,简单易懂嘛

    玩法

    任何人进入游戏输入名字然后就可以连接进入游戏,控制一个小球。

    你可以操作这个小球进行一些动作,比如:移动,发射子弹。

    通过杀死其他玩家来获取积分,并在排行榜上进行排名。

    其实这类游戏有一个统一的名称,叫做IO类游戏,在这个网站中有大量的这类游戏:https://iogames.space/

    这个游戏的github地址:https://github.com/lionet1224/node-game

    在线体验: http://120.77.44.111:3000/

    演示GIF:

    准备工作

    首先制作这个游戏,我们需要的技术为:

    • 前端

      • Socket.io

      • Webpack

    • 后端

      • Node

      • Socket.io

      • express

      • ...

    并且你需要对以下技术有一定了解:

    • Canvas

    • 面向对象

    • ES6

    • Node

    • Promise

    其实本来想使用denots来开发的,但是因为我对这两项技术都是半生不熟的阶段,所以就不拿出来献丑了。

    游戏架构

    后端服务需要做的是:

    • 存储生成的游戏对象,并且将其发送给前端。

    • 接收前端的玩家操作,给游戏对象进行数据处理

    前端需要做的是:

    • 接收后端发送的数据并将其渲染出来。

    • 将玩家的操作发送给服务器

    这也是典型的状态同步方式开发游戏。

    后端服务搭建开发

    因为前端是通过后端的数据驱动的,所以我们就先开发后端。

    搭建起一个Express服务

    首先我们需要下载express,在根目录下输入以下命令:

    // 创建一个package.json文件
    > npm init
    // 安装并且将其置入package.json文件中的依赖中
    > npm install express socket.io --save
    // 安装并置入package.json的开发依赖中
    > npm install cross-env nodemon --save-dev
    
    

    这里我们也可以使用cnpm进行安装

    然后在根目录中疯狂建文件夹以及文件。

    image.png

    我们就可以得出以上的文件啦。

    解释一下分别是什么东西:

    • public 存储一些资源

    • src 开发代码

      • core 核心代码

      • objects 玩家、道具等对象

      • client 前端代码

      • servers 后端代码

      • shared 前后端共用常量

    编写基本代码

    然后我们在server.js中编写启动服务的相关代码。

    // server.js
    // 引入各种模块
    const express = require('express')
    const socketio = require('socket.io');
    const app = express();
    
    const Socket = require('./core/socket');
    const Game = require('./core/game');
    
    // 启动服务
    const port = process.env.PORT || 3000;
    const server = app.listen(3000, () => {
      console.log('Server Listening on port: ' + port)
    })
    
    // 实例游戏类
    const game = new Game;
    
    // 监听socket服务
    const io = socketio(server)
    // 将游戏以及io传入创建的socket类来统一管理
    const socket = new Socket(game, io);
    
    // 监听连接进入游戏的回调
    io.on('connect', item => {
      socket.listen(item)
    })
    
    

    上面的代码还引入了两个其他文件core/gamecore/socket

    这两个文件中的代码,我大致的编写了一下。

    // core/game.js
    class Game{
      constructor(){
        // 保存玩家的socket信息
        this.sockets = {}
        // 保存玩家的游戏对象信息
        this.players = {};
        // 子弹
        this.bullets = [];
        // 最后一次执行时间
        this.lastUpdateTime = Date.now();
        // 是否发送给前端数据,这里将每两帧发送一次数据
        this.shouldSendUpdate = false;
        // 游戏更新
        setInterval(this.update.bind(this), 1000 / 60);
      }
    
      update(){
    
      }
    
      // 玩家加入游戏
      joinGame(){
    
      }
    
      // 玩家断开游戏
      disconnect(){
    
      }
    }
    
    module.exports = Game;
    
    
    // core/socket.js
    const Constants = require('../../shared/constants')
    
    class Socket{
      constructor(game, io){
        this.game = game;
        this.io = io;
      }
    
      listen(){
        // 玩家成功连接socket服务
        console.log(`Player connected! Socket Id: ${socket.id}`)
      }
    }
    
    module.exports = Socket
    
    

    core/socket中引入了常量文件,我们来看看我在其中是怎么定义的。

    // shared/constants.js
    module.exports = Object.freeze({
      // 玩家的数据
      PLAYER: {
        // 最大生命
        MAX_HP: 100,
        // 速度
        SPEED: 500,
        // 大小
        RADUIS: 50,
        // 开火频率, 0.1秒一发
        FIRE: .1
      },
    
      // 子弹
      BULLET: {
        // 子弹速度
        SPEED: 1500,
        // 子弹大小
        RADUIS: 20
      },
    
      // 道具
      PROP: {
        // 生成时间
        CREATE_TIME: 10,
        // 大小
        RADUIS: 30
      },
    
      // 地图大小
      MAP_SIZE: 5000,
    
      // socket发送消息的函数名
      MSG_TYPES: {
        JOIN_GAME: 1,
        UPDATE: 2,
        INPUT: 3
      }
    })
    
    

    Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。- MDN

    通过上面的四个文件的代码,我们已经拥有了一个具备基本功能的后端服务结构了。

    接下来就来将它启动起来吧。

    创建启动命令

    package.json中编写启动命令。

    // package.json
    {
        // ...
        "scripts": {
          "dev": "cross-env NODE_ENV=development nodemon src/servers/server.js",
          "start": "cross-env NODE_ENV=production nodemon src/servers/server.js"
        }
        //..
    }
    
    

    这里的两个命令devstart都使用到了cross-envnodemon,这里解释一下:

    • cross-env 设置环境变量,这里可以看到这个后面还有一个NODE_ENV=development/production,判断是否是开发模式。

    • nodemon 这个的话说白了就是监听文件变化并重置Node服务。

    启动服务看一下吧

    执行以下命令开启开发模式。

    > npm run dev
    
    

    可以看到我们成功的启动服务了,监听到了3000端口。

    在服务中,我们搭载了socket服务,那要怎么测试是否有效呢?

    所以我们现在简单的搭建一下前端吧。

    Webpack搭建前端文件

    我们在开发前端的时候,用到模块化的话会开发更加丝滑一些,并且还有生产环境的打包压缩,这些都可以使用到Webpack

    我们的打包有两种不同的环境,一种是生产环境,一种是开发环境,所以我们需要两个webpack的配置文件。

    当然傻傻的直接写两个就有点憨憨了,我们将其中重复的内容给解构出来。

    我们在根目录下创建webpack.common.jswebpack.dev.jswebpack.prod.js三个文件。

    此步骤的懒人安装模块命令:

    npm install @babel/core @babel/preset-env babel-loader css-loader html-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin terser-webpack-plugin webpack webpack-dev-middleware webpack-merge webpack-cli \--save-dev

    // webpack.common.js
    const path = require('path');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      entry: {
        game: './src/client/index.js',
      },
      // 将打包文件输出到dist文件夹
      output: {
        filename: '[name].[contenthash].js',
        path: path.resolve(__dirname, 'dist'),
      },
      module: {
        rules: [
          // 使用babel解析js
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: "babel-loader",
              options: {
                presets: ['@babel/preset-env'],
              },
            },
          },
          // 将js中的css抽出来
          {
            test: /\.css$/,
            use: [
              {
                loader: MiniCssExtractPlugin.loader,
              },
              'css-loader',
            ],
          },
        ],
      },
      plugins: [
        new MiniCssExtractPlugin({
          filename: '[name].[contenthash].css',
        }),
        // 将处理后的js以及css置入html中
        new HtmlWebpackPlugin({
          filename: 'index.html',
          template: 'src/client/html/index.html',
        }),
      ],
    };
    
    

    上面的代码已经可以处理css以及js文件了,接下来我们将它分配给developmentproduction中,其中production将会压缩jscss以及html

    // webpack.dev.js
    const { merge } = require('webpack-merge')
    const common = require('./webpack.common')
    
    module.exports = merge(common, {
      mode: 'development'
    })
    
    
    // webpack.prod.js
    const { merge } = require('webpack-merge')
    const common = require('./webpack.common')
    // 压缩js的插件
    const TerserJSPlugin = require('terser-webpack-plugin')
    // 压缩css的插件
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
    
    module.exports = merge(common, {
      mode: 'production',
      optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCssAssetsPlugin({})]
      }
    })
    
    

    上面已经定义好了三个不同的webpack文件,那么该怎么样使用它们呢?

    首先开发模式,我们需要做到修改了代码就自动打包代码,那么代码如下:

    // src/servers/server.js
    const webpack = require('webpack')
    const webpackDevMiddleware = require('webpack-dev-middleware')
    
    const webpackConfig = require('../../webpack.dev')
    // 前端静态文件
    const app = express();
    app.use(express.static('public'))
    
    if(process.env.NODE_ENV === 'development'){
      // 这里是开发模式
      // 这里使用了webpack-dev-middleware的中间件,作用就是代码改动就使用webpack.dev的配置进行打包文件
      const compiler = webpack(webpackConfig);
      app.use(webpackDevMiddleware(compiler));
    } else {
      // 上线环境就只需要展示打包后的文件夹
      app.use(express.static('dist'))
    }
    
    

    接下来就在package.json中添加相对应的命令吧。

    {
    //...
      "scripts": {
        "build": "webpack --config webpack.prod.js",
        "start": "npm run build && cross-env NODE_ENV=production nodemon src/servers/server.js"
      },
    //...
    }
    
    

    接下来,我们试试devstart的效果吧。

    可以看到使用npm run dev命令后不仅启动了服务还打包了前端文件。

    再试试npm run start

    也可以看到先打包好了文件再启动了服务。

    我们来看看打包后的文件。

    测试Socket是否有效

    先让我装一下前端的socket.io

    > npm install socket.io-client --save
    
    

    然后编写一下前端文件的入口文件:

    // src/client/index.js
    import { connect } from './networking'
    
    Promise.all([
      connect()
    ]).then(() => {
    
    }).catch(console.error)
    
    

    可以看到上面代码我引入了另一个文件networking,我们来看一下:

    // src/client/networking
    import io from 'socket.io-client'
    
    // 这里判断是否是https,如果是https就需要使用wss协议
    const socketProtocal = (window.location.protocol.includes('https') ? 'wss' : 'ws');
    // 这里就进行连接并且不重新连接,这样可以制作一个断开连接的功能
    const socket = io(`${socketProtocal}://${window.location.host}`, { reconnection: false })
    
    const connectPromise = new Promise(resolve => {
      socket.on('connect', () => {
        console.log('Connected to server!');
        resolve();
      })
    })
    
    export const connect = onGameOver => {
      connectPromise.then(()=> {
        socket.on('disconnect', () => {
          console.log('Disconnected from server.');
        })
      })
    }
    
    

    上面的代码就是连接socket,将会自动获取地址然后进行连接,通过Promise传给index.js,这样入口文件就可以知道什么时候连接成功了。

    我们现在就去前端页面中看一下吧。

    可以很清楚的看到,前后端都有连接成功的相关提示。

    创建游戏对象

    我们现在来定义一下游戏中的游戏对象吧。

    首先游戏中将会有四种不同的游戏对象:

    • Player 玩家人物

    • Prop 道具

    • Bullet 子弹

    我们来一一将其实现吧。

    首先他们都属于物体,所以我给他们都定义一个父类Item:

    // src/servers/objects/item.js
    class Item{
      constructor(data = {}){
        // id
        this.id = data.id;
        // 位置
        this.x = data.x;
        this.y = data.y;
        // 大小
        this.w = data.w;
        this.h = data.h;
      }
    
      // 这里是物体每帧的运行状态
      update(dt){
      
      }
    
      // 格式化数据以方便发送数据给前端
      serializeForUpdate(){
        return {
          id: this.id,
          x: this.x,
          y: this.y,
          w: this.w,
          h: this.h
        }
      }
    }
    
    module.exports = Item;
    
    

    上面这个类是所有游戏对象都要继承的类,它定义了游戏世界里每一个元素的基本属性。

    接下来就是playerPropBullet的定义了。

    // src/servers/objects/player.js
    const Item = require('./item')
    const Constants = require('../../shared/constants')
    
    /**
     * 玩家对象类
     */
    class Player extends Item{
      constructor(data){
        super(data);
    
        this.username = data.username;
        this.hp = Constants.PLAYER.MAX_HP;
        this.speed = Constants.PLAYER.SPEED;
        // 击败分值
        this.score = 0;
        // 拥有的buffs
        this.buffs = [];
      }
    
      update(dt){
    
      }
    
      serializeForUpdate(){
        return {
          ...(super.serializeForUpdate()),
          username: this.username,
          hp: this.hp,
          buffs: this.buffs.map(item => item.type)
        }
      }
    }
    
    module.exports = Player;
    
    

    然后是道具以及子弹的定义。

    // src/servers/objects/prop.js
    const Item = require('./item')
    
    /**
     * 道具类
     */
    class Prop extends Item{
      constructor(){
        super();
      }
    }
    
    module.exports = Prop;
    
    
    // src/servers/objects/bullet.js
    const Item = require('./item')
    
    /**
     * 子弹类
     */
    class Bullet extends Item{
      constructor(){
        super();
      }
    }
    
    module.exports = Bullet
    
    

    上面都是简单的定义,随着开发会逐渐添加内容。

    添加事件发送

    上面的代码虽然已经定义好了,但是还需要使用它,所以在这里我们来开发使用它们的方法。

    在玩家输入名称加入游戏后,需要生成一个Player的游戏对象。

    // src/servers/core/socket.js
    class Socket{
      // ...
      listen(socket){
        console.log(`Player connected! Socket Id: ${socket.id}`);
    
        // 加入游戏
        socket.on(Constants.MSG_TYPES.JOIN_GAME, this.game.joinGame.bind(this.game, socket));
        // 断开游戏
        socket.on('disconnect', this.game.disconnect.bind(this.game, socket));
      }
      // ...
    }
    
    

    然后在game.js中添加相关逻辑。

    // src/servers/core/game.js
    const Player = require('../objects/player')
    const Constants = require('../../shared/constants')
    
    class Game{
      // ...
    
      update(){
        const now = Date.now();
        // 现在的时间减去上次执行完毕的时间得到中间间隔的时间
        const dt = (now - this.lastUpdateTime) / 1000;
        this.lastUpdateTime = now;
    
        // 更新玩家人物
        Object.keys(this.players).map(playerID => {
          const player = this.players[playerID];
          player.update(dt);
        })
    
        if(this.shouldSendUpdate){
          // 发送数据
          Object.keys(this.sockets).map(playerID => {
            const socket = this.sockets[playerID];
            const player = this.players[playerID];
            socket.emit(
                Constants.MSG_TYPES.UPDATE,
                // 处理游戏中的对象数据发送给前端
                this.createUpdate(player)
            )
          })
    
          this.shouldSendUpdate = false;
        } else {
          this.shouldSendUpdate = true;
        }
      }
    
      createUpdate(player){
        // 其他玩家
        const otherPlayer = Object.values(this.players).filter(
            p => p !== player
        );
    
        return {
          t: Date.now(),
          // 自己
          me: player.serializeForUpdate(),
          others: otherPlayer,
          // 子弹
          bullets: this.bullets.map(bullet => bullet.serializeForUpdate())
        }
      }
    
      // 玩家加入游戏
      joinGame(socket, username){
        this.sockets[socket.id] = socket;
    
        // 玩家位置随机生成
        const x = (Math.random() * .5 + .25) * Constants.MAP_SIZE;
        const y = (Math.random() * .5 + .25) * Constants.MAP_SIZE;
        this.players[socket.id] = new Player({
          id: socket.id,
          username,
          x, y,
          w: Constants.PLAYER.WIDTH,
          h: Constants.PLAYER.HEIGHT
        })
      }
    
      disconnect(socket){
        delete this.sockets[socket.id];
        delete this.players[socket.id];
      }
    }
    
    module.exports = Game;
    
    

    这里我们开发了玩家的加入以及退出,还有Player对象的数据更新,以及游戏的数据发送。

    现在后端服务已经有能力提供内容给前端了,接下来我们开始开发前端的界面吧。

    前端界面开发

    上面的内容让我们开发了一个拥有基本功能的后端服务。

    接下来来开发前端的相关功能吧。

    接收后端发送的数据

    我们来看看后端发过来的数据是什么样的吧。

    先在前端编写接收的方法。

    // src/client/networking.js
    import { processGameUpdate } from "./state";
    
    export const connect = onGameOver => {
      connectPromise.then(()=> {
        // 游戏更新
        socket.on(Constants.MSG_TYPES.UPDATE, processGameUpdate);
    
        socket.on('disconnect', () => {
          console.log('Disconnected from server.');
        })
      })
    }
    
    export const play = username => {
      socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
    }
    
    
    // src/client/state.js
    export function processGameUpdate(update){
        console.log(update);
    }
    
    
    // src/client/index.js
    import { connect, play } from './networking'
    
    Promise.all([
      connect()
    ]).then(() => {
      play('test');
    }).catch(console.error)
    
    

    上面的代码就可以让我们进入页面就直接加入游戏了,去页面看看效果吧。

    image.png

    编写游戏界面

    我们先将html代码编辑一下。

    // src/client/html/index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>球球作战</title>
    </head>
    <body>
      <canvas id="cnv"></canvas>
      <div id="home">
        <h1>球球作战</h1>
        <p class="text-secondary">一个简简单单的射击游戏</p>
        <hr>
    
        <div class="content">
          <div class="key">
            <p>
              <code>W</code> 向上移动
            </p>
            <p>
              <code>S</code> 向下移动
            </p>
            <p>
              <code>A</code> 向左移动
            </p>
            <p>
              <code>D</code> 向右移动
            </p>
            <p>
              <code>鼠标左键</code> 发射子弹
            </p>
          </div>
          <div class="play hidden">
            <input type="text" id="username-input" placeholder="名称">
            <button id="play-button">开始游戏</button>
          </div>
          <div class="connect">
            <p>连接服务器中...</p>
          </div>
        </div>
      </div>
    </body>
    </html>
    
    

    然后在index.js中导入css

    // src/client/index.js
    import './css/bootstrap-reboot.css'
    import './css/main.css'
    
    

    src/client/css中创建对应的文件,其中bootstrap-rebootbootstrap的重置基础样式的文件,这个可以在网络上下载,因为太长,本文就不贴出来了。

    main.css中编写对应的样式。

    // src/client/css/main.css
    html, body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      width: 100%;
      height: 100vh;
      background: linear-gradient(to right bottom, rgb(154, 207, 223), rgb(100, 216, 89));
    }
    
    .hidden{
      display: none !important;
    }
    
    #cnv{
      width: 100%;
      height: 100%;
    }
    
    .text-secondary{
      color: #666;
    }
    
    code{
      color: white;
      background: rgb(236, 72, 72);
      padding: 2px 10px;
      border-radius: 5px;
    }
    
    hr {
      border: 0;
      border-top: 1px solid rgba(0, 0, 0, 0.1);
      margin: 1rem 0;
      width: 100%;
    }
    
    button {
      font-size: 18px;
      outline: none;
      border: none;
      color: black;
      background-color: transparent;
      padding: 5px 20px;
      border-radius: 3px;
      transition: background-color 0.2s ease;
    }
    
    button:hover {
      background-color: rgb(141, 218, 134);
      color: white;
    }
    
    button:focus {
      outline: none;
    }
    
    #home p{
      margin-bottom: 5px;
    }
    
    #home{
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translateY(-50%) translateX(-50%);
      padding: 20px 30px;
      background-color: white;
      display: flex;
      flex-direction: column;
      align-items: center;
      border-radius: 5px;
      text-align: center;
    }
    
    #home input {
      font-size: 18px;
      outline: none;
      border: none;
      border-bottom: 1px solid #dedede;
      margin-bottom: 5px;
      padding: 3px;
      text-align: center;
    }
    
    #home input:focus{
      border-bottom: 1px solid #8d8d8d;
    }
    
    #home .content{
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    
    #home .content .play{
      width: 200px;
      margin-left: 50px;
    }
    
    #home .content .connect{
      margin-left: 50px;
    }
    
    

    最后我们就可以得到下面这张图的效果了。

    image.png

    编写游戏开始的逻辑

    我们先创建一个util.js来存放一些工具函数。

    // src/client/util.js
    export function $(elem){
      return document.querySelector(elem)
    }
    
    

    然后在index.js中编写对应的逻辑代码。

    // src/client/index.js
    import { connect, play } from './networking'
    import { $ } from './util'
    
    Promise.all([
      connect()
    ]).then(() => {
      // 隐藏连接服务器显示输入框及按键
      $('.connect').classList.add('hidden')
      $('.play').classList.remove('hidden')
      // 并且默认聚焦输入框
      $('#home input').focus();
    
      // 游戏开始按钮监听点击事件
      $('#play-button').onclick = () => {
        // 判断输入框的值是否为空
        let val = $('#home input').value;
        if(val.replace(/\s*/g, '') === '') {
          alert('名称不能为空')
          return;
        }
        // 游戏开始,隐藏开始界面
        $('#home').classList.add('hidden')
        play(val)
      }
    }).catch(console.error)
    
    

    上面的代码已经可以正常的开始游戏了,但是游戏开始了,没有画面。

    所以,我们现在来开发一下渲染画面的代码。

    加载资源

    我们都知道canvas绘制图片需要图片加载完毕,不然的话会啥也没有,所以我们先编写一个加载所有图片的代码。

    图片文件存储在public/assets

    // src/client/asset.js
    // 需要加载的资源
    const ASSET_NAMES = [
      'ball.svg',
      'aim.svg'
    ]
    
    // 将下载好的图片文件保存起来供canvas使用
    const assets = {};
    // 每一张图片都是通过promise进行加载的,所有图片加载成功后,Promise.all就会结束
    const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset))
    
    function downloadAsset(assetName){
      return new Promise(resolve => {
        const asset = new Image();
        asset.onload = () => {
          console.log(`Downloaded ${assetName}`)
          assets[assetName] = asset;
          resolve();
        }
        asset.src = `/assets/${assetName}`
      })
    }
    
    export const downloadAssets = () => downloadPromise;
    export const getAsset = assetName => assets[assetName]
    
    

    接下来在index.js中引入asset.js

    // src/client/index.js
    import { downloadAssets } from './asset'
    
    Promise.all([
      connect(),
      downloadAssets()
    ]).then(() => {
      // ...
    }).catch(console.error)
    
    

    这个时候,我们在页面中就可以看到这样的输出了。

    image.png

    图片可以去iconfont或是在线体验的network或是github中下载。

    绘制游戏对象

    我们新建一个render.js文件,在其中编写对应的绘制代码。

    // src/client/render.js
    import { MAP_SIZE, PLAYER } from '../shared/constants'
    import { getAsset } from './asset'
    import { getCurrentState } from './state'
    import { $ } from './util'
    
    const cnv = $('#cnv')
    const ctx = cnv.getContext('2d')
    
    function setCanvasSize(){
      cnv.width = window.innerWidth;
      cnv.height = window.innerHeight;
    }
    
    // 这里将默认设置一次canvas宽高,当屏幕缩放的时候也会设置一次
    setCanvasSize();
    window.addEventListener('resize', setCanvasSize)
    
    // 绘制函数
    function render(){
      const { me, others, bullets } = getCurrentState();
      if(!me){
        return;
      }
    }
    
    // 这里将启动渲染函数的定时器,将其导出,我们在index.js中使用
    let renderInterval = null;
    export function startRendering(){
      renderInterval = setInterval(render, 1000 / 60);
    }
    
    export function stopRendering(){
      ctx.clearRect(0, 0, cnv.width, cnv.height)
      clearInterval(renderInterval);
    }
    
    

    可以看到上面我们引入state.js中的getCurrentState函数,这个函数将获取最新服务器返回的数据对象。

    // src/client/state.js
    const gameUpdates = [];
    
    export function processGameUpdate(update){
      gameUpdates.push(update)
    } 
    
    export function getCurrentState(){
      return gameUpdates[gameUpdates.length - 1]
    }
    
    

    绘制背景

    因为游戏中的地图是一个大地图,一个屏幕是装不下的,所以玩家移动需要一个参照物,这里使用一个渐变的圆来做参照物。

    // src/client/render.js
    function render(){
      // ...
      // 绘制背景圆
      renderBackground(me.x, me.y);
    
      // 绘制一个边界
      ctx.strokeStyle = 'black'
      ctx.lineWidth = 1;
      // 默认边界左上角在屏幕中心,减去人物的x/y算出相对于人物的偏移
      ctx.strokeRect(cnv.width / 2 - me.x, cnv.height / 2 - me.y, MAP_SIZE, MAP_SIZE)
    }
    
    function renderBackground(x, y){
      // 假设背景圆的位置在屏幕左上角,那么cnv.width/height / 2就会将这个圆定位在屏幕中心
      // MAP_SIZE / 2 - x/y 地图中心与玩家的距离,这段距离就是背景圆圆心正确的位置
      const backgroundX = MAP_SIZE / 2 - x + cnv.width / 2;
      const backgroundY = MAP_SIZE / 2 - y + cnv.height / 2;
      const bgGradient = ctx.createRadialGradient(
        backgroundX,
        backgroundY,
        MAP_SIZE / 10,
        backgroundX,
        backgroundY,
        MAP_SIZE / 2
      )
      bgGradient.addColorStop(0, 'rgb(100, 216, 89)')
      bgGradient.addColorStop(1, 'rgb(154, 207, 223)')
      ctx.fillStyle = bgGradient;
      ctx.fillRect(0, 0, cnv.width, cnv.height)
    }
    
    

    上面的代码实现的效果就是下图。

    我们玩家的位置在服务器中设置的是随机数字,所以每次进入游戏都是随机的位置。

    image.png

    绘制玩家

    接下来就是绘制玩家了,依旧是在render.js中编写对应的代码。

    // src/client/render.js
    function render(){
      // ...
      // 绘制所有的玩家
      // 第一个参数是对照位置的数据,第二个参数是玩家渲染的数据
      renderPlayer(me, me);
      others.forEach(renderPlayer.bind(null, me));
    }
    
    function renderPlayer(me, player){
      const { x, y } = player;
      // 默认将玩家渲染在屏幕中心,然后将位置设置上去,再计算相对于自己的相对位置,就是正确在屏幕的位置了
      const canvasX = cnv.width / 2 + x - me.x;
      const canvasY = cnv.height / 2 + y - me.y;
    
      ctx.save();
      ctx.translate(canvasX, canvasY);
      ctx.drawImage(
        getAsset('ball.svg'),
        -PLAYER.RADUIS,
        -PLAYER.RADUIS,
        PLAYER.RADUIS * 2,
        PLAYER.RADUIS * 2
      )
      ctx.restore();
    
      // 绘制血条背景
      ctx.fillStyle = 'white'
      ctx.fillRect(
        canvasX - PLAYER.RADUIS,
        canvasY - PLAYER.RADUIS - 8,
        PLAYER.RADUIS * 2,
        4
      )
    
      // 绘制血条
      ctx.fillStyle = 'red'
      ctx.fillRect(
        canvasX - PLAYER.RADUIS,
        canvasY - PLAYER.RADUIS - 8,
        PLAYER.RADUIS * 2 * (player.hp / PLAYER.MAX_HP),
        4
      )
    
      // 绘制玩家的名称
      ctx.fillStyle = 'white'
      ctx.textAlign = 'center';
      ctx.font = "20px '微软雅黑'"
      ctx.fillText(player.username, canvasX, canvasY - PLAYER.RADUIS - 16)
    }
    
    

    这样就可以将玩家正确的绘制出来了。

    image.png
    image.png

    上面两张图,是我打开两个页面进入游戏的两名玩家,可以看出它们分别以自己为中心,其他的玩家相对于它进行了绘制。

    游戏玩法开发

    添加移动交互

    既然玩家我们绘制出来了,那么就可以让它开始移动起来了。

    我们创建一个input.js来编写对应的输入交互代码。

    // src/client/input.js
    // 发送信息给后端
    import { emitControl } from "./networking";
    
    function onKeydown(ev){
      let code = ev.keyCode;
      switch(code){
        case 65:
          emitControl({
            action: 'move-left',
            data: false
          })
          break;
        case 68:
          emitControl({
            action: 'move-right',
            data: true
          })
          break;
        case 87:
          emitControl({
            action: 'move-top',
            data: false
          })
          break;
        case 83:
          emitControl({
            action: 'move-bottom',
            data: true
          })
          break;
      }
    }
    
    function onKeyup(ev){
      let code = ev.keyCode;
      switch(code){
        case 65:
          emitControl({
            action: 'move-left',
            data: 0
          })
          break;
        case 68:
          emitControl({
            action: 'move-right',
            data: 0
          })
          break;
        case 87:
          emitControl({
            action: 'move-top',
            data: 0
          })
          break;
        case 83:
          emitControl({
            action: 'move-bottom',
            data: 0
          })
          break;
      }
    }
    
    export function startCapturingInput(){
      window.addEventListener('keydown', onKeydown);
      window.addEventListener('keyup', onKeyup);
    }
    
    export function stopCapturingInput(){
      window.removeEventListener('keydown', onKeydown);
      window.removeEventListener('keyup', onKeyup);
    }
    
    
    // src/client/networking.js
    // ...
    
    // 发送信息给后端
    export const emitControl = data => {
      socket.emit(Constants.MSG_TYPES.INPUT, data);
    }
    
    

    上面的代码很简单,通过判断W/S/A/D四个按键发送信息给后端。

    后端进行处理传递给玩家对象,然后在游戏更新中使玩家移动。

    // src/servers/core/game.js
    class Game{
      // ...
      update(){
        const now = Date.now();
        const dt = (now - this.lastUpdateTime) / 1000;
        this.lastUpdateTime = now;
    
        // 每次游戏更新告诉玩家对象,你要更新了
        Object.keys(this.players).map(playerID => {
          const player = this.players[playerID]
          player.update(dt)
        })
      }
    
      handleInput(socket, item){
        const player = this.players[socket.id];
        if(player){
          let data = item.action.split('-');
          let type = data[0];
          let value = data[1];
          switch(type){
            case 'move':
              // 这里是为了防止前端发送1000/-1000这种数字,会导致玩家移动飞快
              player.move[value] = typeof item.data === 'boolean'
                                    ? item.data ? 1 : -1
                                    : 0
              break;
          }
        }
      }
    }
    
    

    然后在player.js中加入对应的移动代码。

    // src/servers/objects/player.js
    class Player extends Item{
      constructor(data){
        super(data)
    
        this.move = {
          left: 0, right: 0,
          top: 0, bottom: 0
        };
        // ...
      }
    
      update(dt){
        // 这里的dt是每次游戏更新的时间,乘于dt将会60帧也就是一秒移动speed的值
        this.x += (this.move.left + this.move.right) * this.speed * dt;
        this.y += (this.move.top + this.move.bottom) * this.speed * dt;
      }
    
      // ...
    }
    
    module.exports = Player;
    
    

    通过上面的代码,我们就实现了玩家移动的逻辑了,下面我们看看效果。

    5.gif

    可以看出,我们可以飞出地图之外,我们在player.js中添加对应的限制代码。

    // src/servers/objects/player.js
    class Player extends Item{
      // ...
      
      update(dt){
        this.x += (this.move.left + this.move.right) * this.speed * dt;
        this.y += (this.move.top + this.move.bottom) * this.speed * dt;
    
        // 在地图最大尺寸和自身位置比较时,不能大于地图最大尺寸
        // 在地图开始0位置和自身位置比较时,不能小于0
        this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x))
        this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y))
      }
    
      // ...
    }
    
    module.exports = Player;
    
    

    增加发送子弹

    既然我们的人物已经可以移动了,那么玩家间对抗的工具“子弹”那肯定是不能少的,现在我们就来开发吧。

    我们先在前端添加发送开枪意图的代码。

    // src/client/input.js
    // 这里使用atan2获取鼠标相对屏幕中心的角度
    function getMouseDir(ev){
      const dir = Math.atan2(ev.clientX - window.innerWidth / 2, ev.clientY - window.innerHeight / 2);
      return dir;
    }
    
    // 每次鼠标移动,发送方向给后端保存
    function onMousemove(ev){
      if(ev.button === 0){
        emitControl({
          action: 'dir',
          data: getMouseDir(ev)
        })
      }
    }
    
    // 开火
    function onMousedown(ev){
      if(ev.button === 0){
        emitControl({
          action: 'bullet',
          data: true
        })
      }
    }
    
    // 停火
    function onMouseup(ev){
      if(ev.button === 0){
        emitControl({
          action: 'bullet',
          data: false
        })
      }
    }
    
    export function startCapturingInput(){
      window.addEventListener('mousedown', onMousedown)
      window.addEventListener('mousemove', onMousemove)
      window.addEventListener('mouseup', onMouseup)
    }
    
    export function stopCapturingInput(){
      window.removeEventListener('mousedown', onMousedown)
      window.addEventListener('mousemove', onMousemove)
      window.removeEventListener('mouseup', onMouseup)
    }
    
    

    然后在后端中编写对应的代码。

    // src/servers/core/game.js
    class Game{
      // ...
      
      update(){
        // ...
        // 如果子弹飞出地图或是已经达到人物身上,就过滤掉
        this.bullets = this.bullets.filter(item => !item.isOver)
        // 为每一个子弹更新
        this.bullets.map(bullet => {
          bullet.update(dt);
        })
    
        Object.keys(this.players).map(playerID => {
          const player = this.players[playerID]
          // 在人物对象中添加发射子弹
          const bullet = player.update(dt)
          if(bullet){
            this.bullets.push(bullet);
          }
        })
      }
    
      handleInput(socket, item){
        const player = this.players[socket.id];
        if(player){
          let data = item.action.split('-');
          let type = data[0];
          let value = data[1];
          switch(type){
            case 'move':
              player.move[value] = typeof item.data === 'boolean'
                                    ? item.data ? 1 : -1
                                    : 0
              break;
            // 更新鼠标位置
            case 'dir':
              player.fireMouseDir = item.data;
              break;
            // 开火/停火
            case 'bullet':
              player.fire = item.data;
              break;
          }
        }
      }
    }
    
    module.exports = Game;
    
    

    game.js中已经编写好了子弹的逻辑了,现在只需要在player.js中返回一个bullet对象就可以成功发射了。

    // src/servers/objects/player.js
    const Bullet = require('./bullet');
    
    class Player extends Item{
      constructor(data){
        super(data)
        
        // ...
        
        // 开火
        this.fire = false;
        this.fireMouseDir = 0;
        this.fireTime = 0;
      }
    
      update(dt){
        // ...
        
        // 每帧都减少开火延迟
        this.fireTime -= dt;
        // 判断是否开火
        if(this.fire != false){
          // 如果没有延迟了就返回一个bullet对象
          if(this.fireTime <= 0){
            // 将延迟重新设置
            this.fireTime = Constants.PLAYER.FIRE;
            // 创建一个bullet对象,将自身的id传递过去,后面做碰撞的时候,就自己发射的子弹就不会打到自己
            return new Bullet(this.id, this.x, this.y, this.fireMouseDir);
          }
        }
      }
      
      // ...
    }
    
    module.exports = Player;
    
    

    对应的bullet.js文件也要补全一下。

    // src/servers/objects/bullet.js
    const shortid = require('shortid')
    const Constants = require('../../shared/constants');
    const Item = require('./item')
    
    class Bullet extends Item{
      constructor(parentID, x, y, dir){
        super({
          id: shortid(),
          x, y,
          w: Constants.BULLET.RADUIS,
          h: Constants.BULLET.RADUIS,
        });
    
        this.rotate = 0;
        this.dir = dir;
        this.parentID = parentID;
        this.isOver = false;
      }
    
      update(dt){
        // 使用三角函数将鼠标位置计算出对应的x/y值
        this.x += dt * Constants.BULLET.SPEED * Math.sin(this.dir);
        this.y += dt * Constants.BULLET.SPEED * Math.cos(this.dir);
        
        // 这里是为了让子弹有一个旋转功能,一秒转一圈
        this.rotate += dt * 360;
    
        // 离开地图就将isOver设置为true,在game.js中就会过滤
        if(this.x < 0 || this.x > Constants.MAP_SIZE
          || this.y < 0 || this.y > Constants.MAP_SIZE){
            this.isOver = true;
          }
      }
    
      serializeForUpdate(){
        return {
          ...(super.serializeForUpdate()),
          rotate: this.rotate
        }
      }
    }
    
    module.exports = Bullet;
    
    

    这里引入了一个shortid库,是创建一个随机数的作用

    使用npm install shortid \--save安装

    这个时候,我们就可以正常发射子弹,但是还不能看见子弹。

    那是因为没有写对应的绘制代码。

    // src/client/render.js
    function render(){
      // ...
      
      bullets.map(renderBullet.bind(null, me))
    
      // ...
    }
    
    function renderBullet(me, bullet){
      const { x, y, rotate } = bullet;
      ctx.save();
      // 偏移到子弹相对人物的位置
      ctx.translate(cnv.width / 2 + x - me.x, cnv.height / 2 + y - me.y)
      // 旋转
      ctx.rotate(Math.PI / 180 * rotate)
      // 绘制子弹
      ctx.drawImage(
        getAsset('bullet.svg'),
        -BULLET.RADUIS,
        -BULLET.RADUIS,
        BULLET.RADUIS * 2,
        BULLET.RADUIS * 2
      )
      ctx.restore();
    }
    
    

    这个时候,我们就将发射子弹的功能完成了。

    来看看效果吧。

    6.gif

    碰撞检测

    既然完成了玩家的移动及发送子弹逻辑,现在就可以开发对战最重要的碰撞检测了。

    我们直接在game.js中添加。

    // src/servers/core/game.js
    class Game{
      // ..
      
      update(){
        // ...
    
        // 将玩家及子弹传入进行碰撞检测
        this.collisions(Object.values(this.players), this.bullets);
    
        Object.keys(this.sockets).map(playerID => {
          const socket = this.sockets[playerID]
          const player = this.players[playerID]
          // 如果玩家的血量低于等于0就告诉他游戏结束,并将其移除游戏
          if(player.hp <= 0){
            socket.emit(Constants.MSG_TYPES.GAME_OVER)
            this.disconnect(socket);
          }
        })
    
        // ...
      }
    
      collisions(players, bullets){
        for(let i = 0; i < bullets.length; i++){
          for(let j = 0; j < players.length; j++){
            let bullet = bullets[i];
            let player = players[j];
    
            // 自己发射的子弹不能达到自己身上
            // distanceTo是一个使用勾股定理判断物体与自己的距离,如果距离小于玩家与子弹的半径就是碰撞了
            if(bullet.parentID !== player.id
              && player.distanceTo(bullet) <= Constants.PLAYER.RADUIS + Constants.BULLET.RADUIS
              ){
              // 子弹毁灭
              bullet.isOver = true;
              // 玩家扣血
              player.takeBulletDamage();
              // 这里判断给最后一击使其死亡的玩家加分
              if(player.hp <= 0){
                this.players[bullet.parentID].score++;
              }
              break;
            }
          }
        }
      }
    
      // ...
    }
    
    module.exports = Game;
    
    

    接下来在前端中添加游戏结束的逻辑。

    // src/client/index.js
    // ...
    import { startRendering, stopRendering } from './render'
    import { startCapturingInput, stopCapturingInput } from './input'
    
    Promise.all([
      connect(gameOver),
      downloadAssets()
    ]).then(() => {
      // ...
    }).catch(console.error)
    
    function gameOver(){
      // 停止渲染
      stopRendering();
      // 停止监听
      stopCapturingInput();
      // 将开始界面显示出来
      $('#home').classList.remove('hidden');
      alert('你GG了,重新进入游戏吧。');
    }
    
    

    这个时候我们就可以正常的进行游戏了。

    来看看效果。

    8.gif

    排行榜功能

    既然我们已经完成了正常的游戏基本操作,那么现在需要一个排行来让玩家有游戏体验(啊哈哈哈)。

    我们先在前端把排行榜显示出来。

    我们先在后端添加返回排行榜的数据。

    // src/servers/core/game.js
    class Game{
      // ...
    
      createUpdate(player){
        // ...
    
        return {
          // ...
          leaderboard: this.getLeaderboard()
        }
      }
    
      getLeaderboard(){
        return Object.values(this.players)
          .sort((a, b) => b.score - a.score)
          .slice(0, 10)
          .map(item => ({ username: item.username, score: item.score }))
      }
    }
    
    module.exports = Game;
    
    

    然后在前端中编写一下排行榜的样式。

    // src/client/html/index.html
    // ..
    <body>
      <canvas id="cnv"></canvas>
    
      <div class="ranking hidden">
        <table>
          <thead>
            <tr>
              <th>排名</th>
              <th>姓名</th>
              <th>积分</th>
            </tr>
          </thead>
          <tbody>
          </tbody>
        </table>
      </div>
      
      // ...
    </body>
    </html>
    
    
    // src/client/css/main.css
    // ...
    
    .ranking{
      position: fixed;
      width: 300px;
      background: #333;
      top: 0;
      left: 0;
      color: white;
      padding: 10px;
    }
    
    .ranking table{
      border: 0;
      border-collapse: 0;
      width: 100%;
    }
    
    

    再编写一个渲染数据的函数在render.js中。

    // src/client/render.js
    // ...
    
    export function updateRanking(data){
      let str = '';
    
      data.map((item, i) => {
        str += `
          <tr>
            <td>${i + 1}</td>
            <td>${item.username}</td>
            <td>${item.score}</td>
          <tr>
        `
      })
    
      $('.ranking table tbody').innerHTML = str;
    }
    
    

    最后在state.js中使用这个函数。

    // src/client/state.js
    import { updateRanking } from "./render";
    
    const gameUpdates = [];
    
    export function processGameUpdate(update){
      gameUpdates.push(update)
    
      updateRanking(update.leaderboard) 
    }
    
    // ...
    
    

    现在渲染排行榜是没有问题了,现在到index.js中管理一下排行榜的显示隐藏。

    // src/client/index.js
    // ...
    
    Promise.all([
      connect(gameOver),
      downloadAssets()
    ]).then(() => {
      // ...
    
      $('#play-button').onclick = () => {
        // ...
    
        $('.ranking').classList.remove('hidden')
    
        // ...
      }
    }).catch(console.error)
    
    function gameOver(){
      // ...
      $('.ranking').classList.add('hidden')
      // ...
    }
    
    

    写到这里,排行榜的功能就完成了。

    image.png

    道具开发

    当然游戏现在这样游戏性还是很差的,我们来加几个道具增加一点游戏性吧。

    先将prop.js完善吧。

    // src/servers/objects/prop.js
    const Constants = require('../../shared/constants')
    const Item = require('./item')
    
    class Prop extends Item{
      constructor(type){
        // 随机位置
        const x = (Math.random() * .5 + .25) * Constants.MAP_SIZE;
        const y = (Math.random() * .5 + .25) * Constants.MAP_SIZE;
        super({
          x, y,
          w: Constants.PROP.RADUIS,
          h: Constants.PROP.RADUIS
        });
    
        this.isOver = false;
        // 什么类型的buff
        this.type = type;
        // 持续10秒
        this.time = 10;
      }
    
      // 这个道具对玩家的影响
      add(player){
        switch(this.type){
          case 'speed':
            player.speed += 500;
            break;
        }
      }
      
      // 移除这个道具时将对玩家的影响消除
      remove(player){
        switch(this.type){
          case 'speed':
            player.speed -= 500;
            break;
        }
      }
    
      // 每帧更新
      update(dt){
        this.time -= dt;
      }
    
      serializeForUpdate(){
        return {
          ...(super.serializeForUpdate()),
          type: this.type,
          time: this.time
        }
      }
    }
    
    module.exports = Prop;
    
    

    然后我们在game.js中添加定时添加道具的逻辑。

    // src/servers/core/game.js
    const Constants = require("../../shared/constants");
    const Player = require("../objects/player");
    const Prop = require("../objects/prop");
    
    class Game{
      constructor(){
        // ...
        // 增加一个保存道具的数组
        this.props = [];
        
        // ...
        // 添加道具的计时
        this.createPropTime = 0;
        setInterval(this.update.bind(this), 1000 / 60);
      }
    
      update(){
        // ...
        
        // 这个定时为0时添加
        this.createPropTime -= dt;
        // 过滤掉已经碰撞后的道具
        this.props = this.props.filter(item => !item.isOver)
        // 道具大于10个时不添加
        if(this.createPropTime <= 0 && this.props.length < 10){
          this.createPropTime = Constants.PROP.CREATE_TIME;
          this.props.push(new Prop('speed'))
        }
        
        // ...
    
        this.collisionsBullet(Object.values(this.players), this.bullets);
        this.collisionsProp(Object.values(this.players), this.props)
    
        // ...
      }
    
      // 玩家与道具的碰撞检测
      collisionsProp(players, props){
        for(let i = 0; i < props.length; i++){
          for(let j = 0; j < players.length; j++){
            let prop = props[i];
            let player = players[j];
    
            if(player.distanceTo(prop) <= Constants.PLAYER.RADUIS + Constants.PROP.RADUIS){
              // 碰撞后,道具消失
              prop.isOver = true;
              // 玩家添加这个道具的效果
              player.pushBuff(prop);
              break;
            }
          }
        }
      }
    
      // 这里是之前的collisions,为了和碰撞道具区分
      collisionsBullet(players, bullets){
        // ...
      }
    
      createUpdate(player){
        // ...
        
        return {
          // ...
          props: this.props.map(prop => prop.serializeForUpdate())
        }
      }
    }
    
    module.exports = Game;
    
    

    这里可以将碰撞检测进行优化,将其改造成任何场景都可以使用的碰撞函数,这里是为了方便就直接复制成两个。

    接下来在player.js添加对应的函数。

    // src/servers/objects/player.js
    const Item = require('./item')
    const Constants = require('../../shared/constants');
    const Bullet = require('./bullet');
    
    class Player extends Item{
      // ...
    
      update(dt){
        // ...
      
        // 判断buff是否失效
        this.buffs = this.buffs.filter(item => {
          if(item.time > 0){
            return item;
          } else {
            item.remove(this);
          }
        })
        // buff的持续时间每帧都减少
        this.buffs.map(buff => buff.update(dt));
    
        // ...
      }
    
      // 添加
      pushBuff(prop){
        this.buffs.push(prop);
        prop.add(this);
      }
      
      // ...
    
      serializeForUpdate(){
        return {
          // ...
          buffs: this.buffs.map(item => item.serializeForUpdate())
        }
      }
    }
    
    module.exports = Player;
    
    

    后端需要做的功能已经完成了,现在到前端中添加绘制方面的代码。

    // src/client/render.js
    // ...
    
    function render(){
      const { me, others, bullets, props } = getCurrentState();
      if(!me){
        return;
      }
      
      // ...
      
      // 绘制道具
      props.map(renderProp.bind(null, me))
      
      // ...
    }
    
    // ...
    
    // 绘制道具
    function renderProp(me, prop){
      const { x, y, type } = prop;
      ctx.save();
      ctx.drawImage(
        getAsset(`${type}.svg`),
        cnv.width / 2 + x - me.x,
        cnv.height / 2 + y - me.y,
        PROP.RADUIS * 2,
        PROP.RADUIS * 2
      )
      ctx.restore();
    }
    
    function renderPlayer(me, player){
      // ...
      
      // 显示玩家已经领取到的道具
      player.buffs.map((buff, i) => {
        ctx.drawImage(
          getAsset(`${buff.type}.svg`),
          canvasX - PLAYER.RADUIS + i * 22,
          canvasY + PLAYER.RADUIS + 16,
          20, 20
        )
      })
    }
    
    

    这个时候,加速道具就完成啦。

    如果你需要添加更多道具,可以在prop.js中进行添加,并且在game.js中生成道具的时候把speed改为随机道具的type

    完成后的效果。

    image.png

    断开连接显示

    我们可以写一个界面专门来显示断开连接的提示。

    // src/client/html/index.html
    // ...
    <body>
      // ...
      
      <div class="disconnect hidden">
        <p>与服务器断开连接了</p>
      </div>
    </body>
    
    
    // src/client/css/main.css
    .disconnect{
      position: fixed;
      width: 100%;
      height: 100vh;
      left: 0;
      top: 0;
      z-index: 100;
      background: white;
      display: flex;
      justify-content: center;
      align-items: center;
      color: #444;
      font-size: 40px;
    }
    
    

    再到networking.js中断开连接时显示这个界面。

    // src/client/networking.js
    // ...
    
    export const connect = onGameOver => {
      connectPromise.then(() => {
        // ...
        socket.on('disconnect', () => {
          $('.disconnect').classList.remove('hidden')
          console.log('Disconnected from server.')
        })
      })
    }
    
    // ...
    
    

    这个时候,我们打开游戏,然后关闭游戏服务,游戏就会显示这个界面了。

    image.png

    转自:我系小西几呀

    链接:https://juejin.cn/post/6960096410305822751

    推荐阅读

    53道常见NodeJS基础面试题(附答案)

    关注下方「前端开发博客」,回复 “加群”

    加入我们一起学习,天天进步

    如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

    展开全文
  • 三年一个人使用虚幻引擎(UDK)开发的一个游戏心路 对于我个人来说,完成她的意义不仅在与完成了一个儿时的愿望,也是一次战胜自我的旅程,3年的时光,经历了种种变荡。最终,通过压榨自己的业余时光,学习新语言,...
  • 如何开发一个扫雷小游戏

    千次阅读 2016-11-28 17:03:58
    本文介绍如何用C#开发一个扫雷小游戏
  • 文章链接 http://blog.csdn.net/zhmxy555/article/details/7447864作者:毛星云 邮箱: happylifemxy@qq.com  这节笔记的主要内容是介绍一个完整的回合制游戏demo,而这个demo里面主要突出了游戏里AI的各种思考与...
  • 本专栏是着重于讨论“开发款游戏需要怎样的能力”,以及“如何学习开发游戏所需的所有技能”。欢迎来到知乎专栏《自学游戏开发》的第二篇文章。在开始讨论我们的两主题之前,我认为非常有必要让初学者了解一下:...
  • 作为一个在棋牌游戏开发行业从事多年的程序猿,这些年来看着中国的互联网游戏行业一步步成熟壮大,经历了互联网的热潮期,再到现在的移动互联网引领潮流的这样一个时代。从事棋牌游戏开发的我也从以前的常规PC端游戏...
  • C#游戏开发之炸弹人游戏开发

    千次阅读 2016-07-21 22:02:55
    先从总体上介绍炸弹人游戏开发。 先看一下大概效果: 从图上可以看出这款小游戏要实现:玩家(即游戏中的主角)、玩家的行走、可摧毁墙、不可摧毁墙、隐藏物品、可通过路段、释放炸弹、炸弹爆炸、爆炸后玩
  • 此文写在golang游戏开发学习笔记-创建一个能自由探索的3D世界之后,感兴趣可以先去那篇文章了解一些基础知识,在这篇文章里我们要创建一个简单的2D游戏场景以及配套的人物,并实现人物运动和碰撞检测功能,效果如下 ...
  • 我从中部某省偏僻的小山村来到上海这灯红酒绿的国际大都市,找到第份工作,从事大型网络游戏开发,刚开始负责底层驱动开发,因为技术过硬,基础知识功底扎实,很快从程序开发人员中脱颖而出,公司经理提升我为...
  • 我做游戏开发这八年

    万次阅读 多人点赞 2019-08-25 19:11:08
    实习生第一个游戏2012年,第二个重量级IP游戏2013年,转战手游与创业2014-2015,第一次创业2015,西山居和多益网络2016-2018,第二次创业2018至今,360游戏艺术(岂凡网络)技术总监 简述 这篇文章并不是想教会大家...
  • 如何从0开始开发一个实时联机游戏

    万次阅读 多人点赞 2018-07-15 16:02:28
    这是篇严肃的联机游戏开发入门介绍,本文所述代码开源,文末可获得地址。 关于游戏的实时联机对战,目前是很多游戏开发者研究课题,也延伸出了很多概念,如“状态同步”、“帧同步”,目前很多游戏开发框架也提供...
  • 如何利用Cocos2d-x开发一个游戏

    万次阅读 热门讨论 2012-08-21 00:13:29
    [Cocos2d-x相关教程来源于红孩儿的游戏编程之路 CSDN博客地址:... 如何利用Cocos2d-x开发一个游戏?  这个问题的结果应该是一个流程。我将从一些长期的PC端游戏开发经验结合Cocos2d-x的功能来进行说明。
  • 开发一个数字华容道的小游戏

    万次阅读 2018-01-11 17:20:59
    一开始我准备下一个玩玩的。结果没搜到。所以决定写了一个。最后效果差不多是这样: 思路以及实现 首先,我们应该考虑如何去实现这个效果。细想一下,其实和之前的2048有点像,但是又不是完全一直。于是,便又...
  •  http://blog.csdn.net/zhmxy555/article/details/7318264作者:毛星云 邮箱: happylifemxy@qq.com  在从第节开始看这笔记系列的话,大家会发现,上来就开始讲DirectX相关的内容,但是写了几节之后,又...
  • Unity,游戏。 本文设计了一个第一人称射击游戏的Demo开发过程,文中对射击游戏的基础功能做了一个详细介绍,适用于一个基础框架,可以用于一个射击游戏的完整开发,请品尝。
  • Unity3D游戏开发之使用Unity3D开发2D游戏 ()

    万次阅读 热门讨论 2014-03-17 09:14:17
    今天要和大家分享的是基于Unity3D开发2D游戏,博主一直钟爱于国产武侠RPG,这个我在开始写Unity3D游戏...首先我们创建一个项目,创建新场景,我们首先创建一个Plane并将其命名为MainMap,然后我们在项目中添加一个Resou
  • 在博客中看到篇PC Vr讲解很独到的文章希望大家可以喜欢 出处:http://ms.csdn.net/geek/91921 VR现在似乎是火热的话题,但在国内又毫无意外成了看不清的迷局,就像充满极客精神的比特币来到这片神奇的土地...
  • 开发游戏流程

    千次阅读 2018-09-07 18:45:56
    开发游戏流程 _1. 游戏公司框架 游戏团队中的角色: 部门 职位 职责 游戏策划组 策划 设计游戏的玩法 游戏美术部 美术 制作游戏需要的美术资源 游戏程序部 程序 编写游戏逻辑,整合...
  • html5多人在线游戏开发

    千次阅读 2016-01-14 10:48:12
    开发中,这坑需要填埋。参考云风的博客: 这里写链接内容 从网络游戏中学习如何处理延迟 ...MMORPG服务器架构MMORPG大型游戏设计与开发(服务器 游戏场景 地图和区域)上面大堆资料,所以说,学到老活到老
  • Windows游戏开发感想&&一个完整的Windows窗口程序

    千次阅读 多人点赞 2017-03-31 15:18:28
    现在的互联网行业从事Windows游戏开发实在是太少了。是因为门槛较高,游戏行业本来就是互联网行业最尖端的领域,要想从事游戏开发,不仅要掌握一门过硬的语言,尤其是对C++的精通,还要对算法非常精通,深刻...
  • html5游戏开发-零基础开发RPG游戏-开源讲座()

    万次阅读 多人点赞 2011-12-13 13:49:21
    因为上篇雷电的开发中,有朋友反应不太理解,本篇将以零基础的视点,来讲解如何开发一款RPG游戏。在游戏的世界里,我们可以看到各种地图,各种游戏人物,看到人物在地图上行走,对话等,无论是地图还是人物,其实...
  • 懒骨头(http://blog.csdn.net/iamlazybone QQ:124774397 )Cocos...来自游戏开发基友群听雨分享的例子,一个横版过关游戏,有带简单ai的敌人,有横版的地图,有操控摇杆,主角的攻击效果,有很多新鲜的东西,开搞!
  • 游戏开发游戏开发书籍汇总

    千次阅读 多人点赞 2018-09-10 20:38:30
    1.《游戏设计的艺术》 2.《游戏设计的100个原理》 ...人类的大脑通常是一个贪婪的模式吞噬者,是一个吞吃概念的柔软矮胖的灰色小精灵。   8.《3D游戏编程大师技巧》 9.《游戏设计-原理与实...
  • Java开发游戏脚本(第卷)

    千次阅读 多人点赞 2020-03-30 18:01:39
    Java开发游戏脚本第卷开发前言开发热身获取鼠标的坐标信息鼠标信息数据处理颜色对比判断鼠标操作多线程启动脚本 开发前言 我以前热爱玩游戏,所以选了软件工程专业,现在是大二学生,学Java有一年了,随着深入的...
  • 首先,要做一款游戏,如果你要做一款有网络的棋牌游戏,有用户数据保存的,那么首先就要有一个服务器,然后我们才能基于unity开发的这个棋牌客户端去跟服务器通信,(如果是做单机,那么就请忽略这第一步)我们使用...
  • 从RTS游戏游戏开发

    千次阅读 多人点赞 2018-11-25 00:14:13
    游戏最大的魅力就是,很多开发的道理,往往要以几年为周期才能有深刻认识,但是游戏几周便有同样的认识了。 好想憋大舰去扫荡啊!! 直接憋终极兵种,一路扫荡,这应该是早期的很多rts玩家爱用的套路。 而且...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 312,621
精华内容 125,048
关键字:

一个人开发游戏