热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

利用HTML5Canvas实现打飞机游戏-

这篇文章主要介绍了利用HTML5Canvas制作一个简单的打飞机游戏,作者也给出了相关的Javascript代码,需要的朋友可以参考下
这篇文章主要介绍了利用HTML5 Canvas制作一个简单的打飞机游戏,作者也给出了相关的Javascript代码,需要的朋友可以参考下

之前在当耐特的DEMO里看到个打飞机的游戏,然后就把他的图片和音频扒了了下来。。。。自己凭着玩的心情重新写了一个。仅供娱乐哈。。。。。。我没有用框架,所有js都是自己写的。。。。。。所以就可以来当个简单的教程,对那些刚玩canvas的,或许能有些帮助,楼主玩canvas也不是很久,技术不是很好,请见谅哈。

  闲话不多说,先上DEMO撒:飞机游戏 楼主写这个人纯碎娱乐,没想着写成多正式的游戏哈。

  步入主题啦:打飞机游戏文件有index.html入口文件,allSprite.js精灵的逻辑处理文件,loading.js加载处理文件以及data.js(初始化的一些数据)。

  首先,正常的游戏基本上都需要一个loading,loading页面就是用来预加载数据的,包括精灵表图片,音频等,因为这是个小游戏,要加载的就只有一些音频和图片。里面的加载代码主要就下面这些,其他是制作loading动画的,那个比较简单,就不贴了,如果有兴趣的直接在DEMO里看控制台就行了:

loadImg:function(datas){   
            var _this = this;   
            var dataIndex = 0;   
            li();   
            function li(){   
                if(datas[dataIndex].indexOf("mp3")>=0){   
                    var audio = document.createElement("audio");   
                    document.body.appendChild(audio);   
                    audio.preload = "auto";   
                    audio.src = datas[dataIndex];   
                    audio.Oncanplaythrough= function(){   
                        this.Oncanplaythrough= null;   
                        dataIndex++;   
                        if(dataIndex===datas.length){   
                            _this.percent = 100;   
                        }else {   
                            _this.percent = parseInt(dataIndex/datas.length*100);   
                            li.call(_this);   
                        }   
                    }   
                }else {   
                    preLoadImg(datas[dataIndex] , function(){   
                        dataIndex++;   
                        if(dataIndex===datas.length){   
                            _this.percent = 100;   
                        } else {   
                            _this.percent = parseInt(dataIndex/datas.length*100);   
                            li.call(_this);   
                        }   
                    })   
                }   
            }   
        },   
//再贴出preLoadImg的方法   
function preLoadImg(src , callback){   
    var img = new Image();   
    img.src = src;   
    if(img.complete){   
        callback.call(img);   
    }else {   
        img.Onload= function(){   
            callback.call(img);   
        }   
    }   
}

我先在data.js里面用一个数组保存文件的链接,然后判断这些链接是图片还是音频,如果是图片就用preLoadImg加载,预加载图片的代码很简单,就是new一个图片对象,然后把链接赋给它,加载完后再回调。音频的加载则是通过生成一个HTML5的audio dom对象,把链接赋给它,audio有一个事件“canplaythrough”,浏览器预计能够在不停下来进行缓冲的情况下持续播放指定的音频/视频时,会发生 canplaythrough 事件,也就是说当canplaythrough被调用时,音频就已经被加载的差不多了,可以进行下一个音频的加载了。就这样当把所有东西都加载完后,再进行回调,开始游戏。

  游戏开始了,一个游戏,会需要很多的对象,所以我就统一写成了一个精灵对象,不同对象之间的每一帧的运动情况直接用behavior来分别编写就行了。

W.Sprite = function(name , painter , behaviors , args){   
    if(name !== undefined) this.name = name;   
    if(painter !== undefined) this.painter = painter;   
    this.top = 0;   
    this.left = 0;   
    this.width = 0;   
    this.height = 0;   
    this.velocityX = 3;   
    this.velocityY = 2;   
    this.visible = true;   
    this.animating = false;   
    this.behaviors = behaviors;   
    this.rotateAngle = 0;   
    this.blood = 50;   
    this.fullBlood = 50;   
    if(name==="plan"){   
        this.rotateSpeed = 0.05;   
        this.rotateLeft = false;   
        this.rotateRight = false;   
        this.fire = false;   
        this.firePerFrame = 10;   
        this.fireLevel = 1;   
    }else if(name==="star"){   
        this.width = Math.random()*2;   
        this.speed = 1*this.width/2;   
        this.lightLength = 5;   
        this.cacheCanvas = document.createElement("canvas");   
        thisthis.cacheCtx = this.cacheCanvas.getContext('2d');   
        thisthis.cacheCanvas.width = this.width+this.lightLength*2;   
        thisthis.cacheCanvas.height = this.width+this.lightLength*2;   
        this.painter.cache(this);   
    }else if(name==="badPlan"){   
        this.badKind = 1;   
        this.speed = 2;   
        this.rotateAngle = Math.PI;   
    }else if(name==="missle"){   
        this.width = missleWidth;   
    }else if(name==="boom"){   
        this.width = boomWidth;   
    }else if(name==="food"){   
        this.width = 40;   
        this.speed = 3;   
        this.kind = "LevelUP"
    }   
    this.toLeft = false;   
    this.toTop = false;   
    this.toRight = false;   
    this.toBottom = false;   
    this.outArcRadius = Math.sqrt((this.width/2*this.width/2)*2);   
    if(args){   
        for(var arg in args){   
            this[arg] = args[arg];   
        }   
    }   
}   
Sprite.prototype = {   
    constructor:Sprite,   
    paint:function(){   
        if(this.name==="badPlan"){this.update();}   
        if(this.painter !== undefined && this.visible){   
            if(this.name!=="badPlan") {   
                this.update();   
            }   
            if(this.name==="plan"||this.name==="missle"||this.name==="badPlan"){   
                ctx.save();   
                ctx.translate(this.left , this.top);   
                ctx.rotate(this.rotateAngle);   
                this.painter.paint(this);   
                ctx.restore();   
            }else {   
                this.painter.paint(this);   
            }   
        }   
    },   
    update:function(time){   
        if(this.behaviors){   
            for(var i=0;i

写出精灵类后,就可以通过编写每个的painter以及behavior来生成不同的对象了。接下来就是写painter了,painter分成两种,一种是普通的painter,一种就是精灵表painter,因为像爆炸动画,飞机开枪动画,都不是一张图片就能搞定的,所以就需要用到精灵表了:

而绘制这些就要为他们定制一个精灵表绘制器,下面这个是最简单的精灵表绘制器,针对游戏的复杂性可以相对的修改精灵表写法,直到合适,不过原理都大同小异,就是小修小改而已:

var SpriteSheetPainter = function(cells){   
            this.cells = cells || [];   
            this.cellIndex = 0;   
        }   
        SpriteSheetPainter.prototype = {   
            advance:function(){   
                if(this.cellIndex === this.cells.length-1){   
                    this.cellIndex = 0;   
                }   
                else this.cellIndex++;   
            },   
            paint:function(sprite){   
                var cell = this.cells[this.cellIndex];   
                context.drawImage(spritesheet , cell.x , cell.y , cell.w , cell.h , sprite.left , sprite.top , cell.w , cell.h);   
            }   
        }

而普通的绘制器就更简单了,直接写一个painter,把要画的什么东西都写进去就行了。

有了精灵类和精灵表绘制器后,我们就可以把星星,飞机,子弹,爆炸对象都写出来了:下面是整个allSprite.js的代码:

(function(W){   
    "use strict"
    var planWidth = 24,   
        planHeight = 24,   
        missleWidth = 70,   
        missleHeight = 70,   
        boomWidth = 60;   
    //精灵类 
    W.Sprite = function(name , painter , behaviors , args){   
        if(name !== undefined) this.name = name;   
        if(painter !== undefined) this.painter = painter;   
        this.top = 0;   
        this.left = 0;   
        this.width = 0;   
        this.height = 0;   
        this.velocityX = 3;   
        this.velocityY = 2;   
        this.visible = true;   
        this.animating = false;   
        this.behaviors = behaviors;   
        this.rotateAngle = 0;   
        this.blood = 50;   
        this.fullBlood = 50;   
        if(name==="plan"){   
            this.rotateSpeed = 0.05;   
            this.rotateLeft = false;   
            this.rotateRight = false;   
            this.fire = false;   
            this.firePerFrame = 10;   
            this.fireLevel = 1;   
        }else if(name==="star"){   
            this.width = Math.random()*2;   
            this.speed = 1*this.width/2;   
            this.lightLength = 5;   
            this.cacheCanvas = document.createElement("canvas");   
            this.cacheCtx = this.cacheCanvas.getContext('2d');   
            this.cacheCanvas.width = this.width+this.lightLength*2;   
            this.cacheCanvas.height = this.width+this.lightLength*2;   
            this.painter.cache(this);   
        }else if(name==="badPlan"){   
            this.badKind = 1;   
            this.speed = 2;   
            this.rotateAngle = Math.PI;   
        }else if(name==="missle"){   
            this.width = missleWidth;   
        }else if(name==="boom"){   
            this.width = boomWidth;   
        }else if(name==="food"){   
            this.width = 40;   
            this.speed = 3;   
            this.kind = "LevelUP"
        }   
        this.toLeft = false;   
        this.toTop = false;   
        this.toRight = false;   
        this.toBottom = false;   
        this.outArcRadius = Math.sqrt((this.width/2*this.width/2)*2);   
        if(args){   
            for(var arg in args){   
                this[arg] = args[arg];   
            }   
        }   
    }   
    Sprite.prototype = {   
        constructor:Sprite,   
        paint:function(){   
            if(this.name==="badPlan"){this.update();}   
            if(this.painter !== undefined && this.visible){   
                if(this.name!=="badPlan") {   
                    this.update();   
                }   
                if(this.name==="plan"||this.name==="missle"||this.name==="badPlan"){   
                    ctx.save();   
                    ctx.translate(this.left , this.top);   
                    ctx.rotate(this.rotateAngle);   
                    this.painter.paint(this);   
                    ctx.restore();   
                }else {   
                    this.painter.paint(this);   
                }   
            }   
        },   
        update:function(time){   
            if(this.behaviors){   
                for(var i=0;i40){   
                    this.advance();   
                    this.dateCount = newd;   
                }   
            }   
            if(this.cellIndexsprite.firePerFrame){   
                    this.advance();   
                    this.dateCount = newd;   
                }   
            }   
            var cell = this.cells[this.cellIndex];   
            ctx.drawImage(this.spritesheet , cell.x , cell.y , cell.w , cell.h , -planWidth/2 , -planHeight/2 , cell.w , cell.h);   
        }   
    }   
    W.planBehavior = [   
        {execute:function(sprite,time){   
            if(sprite.toTop){   
                sprite.top = sprite.topcanvas.width-planWidth/2? sprite.left : sprite.left+sprite.velocityX;   
            }   
            if(sprite.toBottom){   
                sprite.top = sprite.top>canvas.height-planHeight/2? sprite.top : sprite.top+sprite.velocityY;   
            }   
            if(sprite.rotateLeft){   
                sprite.rotateAngle -= sprite.rotateSpeed;   
            }   
            if(sprite.rotateRight){   
                sprite.rotateAngle += sprite.rotateSpeed;   
            }   
            if(sprite.fire&&!sprite.painter.isActive){   
                sprite.painter.isActive = true;   
                this.shot(sprite);   
            }   
        },   
        shot:function(sprite){   
            this.addMissle(sprite , sprite.rotateAngle);   
            var missleAngle = 0.1   
            for(var i=1;i=0&&audio[i].paused){   
                    audio[i].play();   
                    break;   
                }   
            }   
        },   
        addMissle:function(sprite , angle){   
                for(var j=0;j canvas.height){   
                sprite.left = Math.random()*canvas.width;   
                sprite.top = Math.random()*canvas.height - canvas.height;   
            }   
            sprite.top += sprite.speed;   
        }}   
    ]   
    W.starPainter = {   
        paint:function(sprite){   
            ctx.drawImage(sprite.cacheCanvas , sprite.left-sprite.width/2-sprite.lightLength , sprite.top-sprite.width/2-sprite.lightLength)   
        },   
        cache:function(sprite){   
            sprite.cacheCtx.save();   
            var opacity = 0.5,addopa = 1/sprite.lightLength;   
            sprite.cacheCtx.fillStyle = "rgba(255,255,255,0.8)";   
            sprite.cacheCtx.beginPath();   
            sprite.cacheCtx.arc(sprite.width/2+sprite.lightLength , sprite.width/2+sprite.lightLength , sprite.width/2 , 0 , 2*Math.PI);   
            sprite.cacheCtx.fill();   
            for(var i=1;i<=sprite.lightLength;i+=2){   
                opacity-=addopa;   
                sprite.cacheCtx.fillStyle = "rgba(255,255,255,"+opacity+")";   
                sprite.cacheCtx.beginPath();   
                sprite.cacheCtx.arc(sprite.width/2+sprite.lightLength , sprite.width/2+sprite.lightLength , sprite.width/2+i , 0 , 2*Math.PI);   
                sprite.cacheCtx.fill();   
            }   
        }   
    }   
    W.foodBehavior = [   
        {execute:function(sprite,time){   
            sprite.top += sprite.speed;   
            if(sprite.top > canvas.height+sprite.width){   
                sprite.visible = false;   
            }   
        }}   
    ]   
    W.foodPainter = {   
        paint:function(sprite){   
            ctx.fillStyle = "rgba("+parseInt(Math.random()*255)+","+parseInt(Math.random()*255)+","+parseInt(Math.random()*255)+",1)"
            ctx.fOnt="15px 微软雅黑"
            ctx.textAlign = "center";   
            ctx.textBaseline = "middle";   
            ctx.fillText(sprite.kind , sprite.left , sprite.top);   
        }   
    }   
    W.missleBehavior = [{   
        execute:function(sprite,time){   
            sprite.left -= sprite.velocityX;   
            sprite.top -= sprite.velocityY;   
            if(sprite.left<-missleWidth/2||sprite.top<-missleHeight/2||sprite.left>canvas.width+missleWidth/2||sprite.top<-missleHeight/2){   
                sprite.visible = false;   
            }   
        }   
    }];   
    W.misslePainter = {   
        paint:function(sprite){   
            var img = new Image();   
            img.src="../planGame/image/plasma.png"
            ctx.drawImage(img , -missleWidth/2+1 , -missleHeight/2+1 , missleWidth , missleHeight);   
        }   
    }   
    W.badPlanBehavior = [{   
        execute:function(sprite,time){   
            if(sprite.top > canvas.height || !sprite.visible){   
                var random = Math.random();   
                if(point>=200&&point<400){   
                    sprite.fullBlood = 150;   
                    if(random<0.1){   
                        sprite.badKind = 2;   
                        sprite.fullBlood = 250;   
                    }   
                }else if(point>=400&&point<600){   
                    sprite.fullBlood = 250;   
                    if(random<0.2){   
                        sprite.badKind = 2;   
                        sprite.fullBlood = 400;   
                    }   
                    if(random<0.1){   
                        sprite.badKind = 3;   
                        sprite.fullBlood = 600;   
                    }   
                }else if(point>=600){   
                    sprite.fullBlood = 500;   
                    if(random<0.4){   
                        sprite.badKind = 2;   
                        sprite.fullBlood = 700;   
                    }   
                    if(random<0.2){   
                        sprite.badKind = 3;   
                        sprite.fullBlood = 1000;   
                    }   
                }   
                sprite.visible = true;   
                sprite.blood = sprite.fullBlood;   
                sprite.left = Math.random()*(canvas.width-2*planWidth)+planWidth;   
                sprite.top = Math.random()*canvas.height - canvas.height;   
            }   
            sprite.top += sprite.speed;   
        },   
        shot:function(sprite){   
            this.addMissle(sprite , sprite.rotateAngle);   
            var missleAngle = 0.1   
            for(var i=1;i

这些绘制方法之类的都相对比较简单。

  主要说一下飞机的运动以及对象数量的控制,飞机怎么运动?毫无疑问,通过键盘控制它运动,可能很多人就会想到通过keydown这个方法按下的时候通过判断keyCode来让飞机持续运动。但是有个问题,keydown事件不支持多键按下,也就是说,当你按下X键时,keyCode是88,与此同时你按下方向键后,keyCode会瞬间变成37,也就是说,如果你单纯的想靠keydown来控制飞机运动,飞机就只能做一件事,要么只可以往某个方向移动,要么只会开枪。

  所以,我们要通过keydown和keyup来实现飞机的运动,原理很容易理解:当我们按下往左的方向键时,我们给飞机一个往左的状态,也就是让飞机的toLeft属性为true,而在动画循环中,判断飞机的状态,如果toLeft为true则飞机的x值不停地减少,飞机也就会不停地往左移动,然后当我们抬起手指时触发keyup事件,我们就再keyup事件中解除飞机往左的状态。飞机也就停止往左移动了。其他状态也一样的原理,这样写的话,就能够让飞机多种状态于一生了。可以同时开枪同时到处跑了。

实现的代码如下:

//keydown/keyup事件的绑定     
  window.Onkeydown= function(event){   
            switch(event.keyCode){   
                case 88:myplan.fire = true;   
                break;   
                case 90:myplan.rotateLeft=true;   
                break;   
                case 67:myplan.rotateRight=true;   
                break;   
                case 37:myplan.toLeft = true;   
                break;   
                case 38:myplan.toTop = true;   
                break;   
                case 39:myplan.toRight = true;   
                break;   
                case 40:myplan.toBottom = true;   
                break;   
            }   
        }   
        window.Onkeyup= function(event){   
            switch(event.keyCode){   
                case 88:myplan.fire = false;   
                break;   
                case 90:myplan.rotateLeft=false;   
                break;   
                case 67:myplan.rotateRight=false;   
                break;   
                case 37:myplan.toLeft = false;   
                break;   
                case 38:myplan.toTop = false;   
                break;   
                case 39:myplan.toRight = false;   
                break;   
                case 40:myplan.toBottom = false;   
                break;   
            }   
        }       
//飞机每一帧的状态更新处理代码   
execute:function(sprite,time){   
            if(sprite.toTop){   
                spritesprite.top = sprite.topcanvas.width-planWidth/2? sprite.left : sprite.left+sprite.velocityX;   
            }   
            if(sprite.toBottom){   
                spritesprite.top = sprite.top>canvas.height-planHeight/2? sprite.top : sprite.top+sprite.velocityY;   
            }   
            if(sprite.rotateLeft){   
                sprite.rotateAngle -= sprite.rotateSpeed;   
            }   
            if(sprite.rotateRight){   
                sprite.rotateAngle += sprite.rotateSpeed;   
            }   
            if(sprite.fire&&!sprite.painter.isActive){   
                sprite.painter.isActive = true;   
                this.shot(sprite);   
            }

就是如此简单。

  然后说下对象控制,打飞机游戏,会发射大量子弹,产生大量对象,包括爆炸啊,飞机啊,子弹等,如果不停地进行对象的生成和销毁,会让浏览器的负荷变得很大,运行了一段时间后就会卡出翔了。所以,我们要用可以循环利用的对象来解决这个问题,不进行对象的销毁,对所有对象进行保存,循环利用。

  我的做法就是,在游戏初始化的时候,直接生成一定数量的对象,存放在数组里面。当我们需要一个对象的时候,就从里面取,当用完后,再放回数组里面。数组里的所有对象都有一个属性,visible,代表对象当前是否可用。

  举个例子,当我的飞机发射一发炮弹,我需要一发炮弹,所以我就到炮弹数组里遍历,如果遍历到的炮弹visible为true,也就说明该对象正在使用着,不能拿来用,所以继续遍历,直到遍历到visible为false的炮弹对象,说明这个对象暂时没人用。然后就可以拿过来重新设置属性,投入使用了。当炮弹击中敌人或者打出画布外的时候,把炮弹的visible设成false,又成了一个没人用的炮弹在数组里存放起来等待下一次调用。

  所以,我们要预算算好页面大概要用到多少个对象,然后就预先准备好对象,这样,在游戏进行中,不会有对象进行生成和销毁,对游戏性能方面就有了提升了。

    最后再说下音频,游戏里面要用到多个同样的audio才能保证音效的不间断性:

var audio = document.getElementsByTagName("audio");   
                                            for(var i=0;i=0&&audio[i].paused){   
                                                    audio[i].play();   
                                                    break;   
                                                }   
                                            }

好吧,基本上就这样了。技术或许还不够好,纯碎做个记录,如果代码有不当正处,欢迎指出,共同学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!


推荐阅读
  • 本文分享了作者在使用LaTeX过程中的几点心得,涵盖了从文档编辑、代码高亮、图形绘制到3D模型展示等多个方面的内容。适合希望深入了解LaTeX高级功能的用户。 ... [详细]
  • 解决UIScrollView自动偏移问题的方法
    本文介绍了一种有效的方法来解决在使用UIScrollView时出现的自动向下偏移的问题,通过调整特定的属性设置,可以确保滚动视图正常显示。 ... [详细]
  • 本文提供了处理WordPress网站中出现过多重定向问题的方法,包括检查DNS配置、安装SSL证书以及解决数据库连接错误等步骤。 ... [详细]
  • 本文探讨了一种常见的C++面试题目——实现自己的String类。通过此过程,不仅能够检验开发者对C++基础知识的掌握程度,还能加深对其高级特性的理解。文章详细介绍了如何实现基本的功能,如构造函数、析构函数、拷贝构造函数及赋值运算符重载等。 ... [详细]
  • 随着Linux操作系统的广泛使用,确保用户账户及系统安全变得尤为重要。用户密码的复杂性直接关系到系统的整体安全性。本文将详细介绍如何在CentOS服务器上自定义密码规则,以增强系统的安全性。 ... [详细]
  • 本文详细介绍如何安装和配置DedeCMS的移动端站点,包括新版本安装、老版本升级、模板适配以及必要的代码修改,以确保移动站点的正常运行。 ... [详细]
  • 探讨了在HTML表单中使用元素代替进行表单提交的方法。 ... [详细]
  • JavaScript 跨域解决方案详解
    本文详细介绍了JavaScript在不同域之间进行数据传输或通信的技术,包括使用JSONP、修改document.domain、利用window.name以及HTML5的postMessage方法等跨域解决方案。 ... [详细]
  • 3DSMAX制作超现实的体育馆模型
    这篇教程是向脚本之家的朋友介绍3DSMAX制作超现实的体育馆模型方法,教程制作出来的体育馆模型非常地不错,不过教程有点难度,需要有一定基础的朋友学习,推荐到脚本之家,喜欢的朋友可 ... [详细]
  • 解决JavaScript中法语字符排序问题
    在开发一个使用JavaScript、HTML和CSS的Web应用时,遇到从SQLite数据库中提取的法语词汇排序不正确的问题,特别是带重音符号的字母未按预期排序。 ... [详细]
  • 本文详细探讨了使用纯JavaScript开发经典贪吃蛇游戏的技术细节和实现方法。通过具体的代码示例,深入解析了游戏逻辑、动画效果及用户交互的实现过程,为开发者提供了宝贵的参考和实践经验。 ... [详细]
  • 本文探讨了如何利用 jQuery 的 JSONP 技术实现跨域调用外部 Web 服务。通过详细解析 JSONP 的工作原理及其在 jQuery 中的应用,本文提供了实用的代码示例和最佳实践,帮助开发者解决跨域请求中的常见问题。 ... [详细]
  • 将JavaScript文件嵌入HTML文档是Web开发中的基本操作。常见的方法是通过在HTML文件中使用``标签来引用外部的.js文件。这种方法不仅保持了代码的整洁性,还便于管理和维护。此外,还可以利用模块化脚本和异步加载技术进一步提升页面性能。 ... [详细]
  • 本文探讨了如何通过检测浏览器类型来动态加载特定的npm包,从而优化前端性能。具体而言,仅在用户使用Edge浏览器时加载相关包,以提升页面加载速度和整体用户体验。此外,文章还介绍了实现这一目标的技术细节和最佳实践,包括使用User-Agent字符串进行浏览器识别、条件加载策略以及性能监控方法。 ... [详细]
  • 在 Angular Google Maps 中实现图片嵌入信息窗口的功能,可以通过使用 `@agm/core` 库来实现。该库提供了丰富的 API 和组件,使得开发者可以轻松地在地图上的信息窗口中嵌入图片。本文将详细介绍如何配置和使用这些组件,以实现动态加载和显示图片的功能。此外,还将探讨一些常见的问题和解决方案,帮助开发者更好地集成这一功能。 ... [详细]
author-avatar
手机用户2402852307
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有