web版拳皇,使用html,css,js来制作一款拳皇游戏
游戏简介
《拳皇》是1994年日本SNK公司旗下在MVS游戏机板上发售的一款著名对战型格斗街机游戏,简称"KOF",也是在剧情中举行的世界规模的格斗大赛的名称。最初为该公司旗下另外两部作品《饿狼传说》系列和《龙虎之拳2》中以南镇为舞台举办的格斗大赛。“KOF”为官方的简称,取自每个单词的首字母。商标注册于SNK公司的名下。中国大陆和中国香港称"拳皇"。游戏官方公认的中译名为"拳皇"(可从SNK Playmore中国香港官网或部份系列作中的部份背景的文字得知)。中国台湾译为"格斗之王",则是来自当初游戏机台招式贴纸的译名。
截至2022年,共发行15部拳皇正统游戏作品:拳皇94、拳皇95、拳皇96、拳皇97、拳皇98、拳皇99、拳皇2000、拳皇2001、拳皇2002、拳皇2003、拳皇XI(拳皇11)、拳皇XII(拳皇12)、拳皇XIII(拳皇13)、拳皇XIV(拳皇14)、拳皇XV(拳皇15) [3] 。
背景设定
自1994年开始每年都有发表新作这一传统直到2003年为止,之后正统续作不再以年代标记,且续作的发布时间不定。但是基本上每年都有新作发表。至KOF2003为止,作品均以SNK自家开发的基板”MVS”开发,而且这些作品都有在和MVS基板的互换游戏机”NEO-GEO”中移植。
最初游戏的卖点为《饿狼传说》VS《龙虎之拳》,以及融汇了1980年代街机游戏《怒》、《超能力战士》等该公司的招牌游戏角色出场争夺冠军的梦幻设定(在XIV之前,侍魂的角色以世界观不合为理由,在正篇作品没有正式参战的)。游戏模式为玩家使用3人一组(NESTS篇为4人一组)的队伍与其他队伍进行淘汰赛这样的游戏模式。由于一次可玩的人物是一般1V1格斗游戏的三倍,因此增加了游戏性。
拳皇
拳皇
另一方面,虽然是同社明星大集合,但游戏中也有原创人物和原创故事,故事、设定也是脍炙人口的要素之一。如三神器(草薙京、八神庵、神乐千鹤),OROCHI一族,NESTS等KOF原创角色也有着很高的人气。每一次新作的团队构成,都有在曾经的竞争对手这个设定上做出变化,以及增加新的宿敌来体现战斗的巨大魅力。新角色的持续增加,到现在为止登场的角色长达100人。该系列作在90年代中期街机黄金期中受到巨大欢迎的新作,如今的新作也释放着同样的魅力。(其实当年在KOF系列推出前各大传媒都不看好这款游戏,但推出后大受欢迎,结果成为了SNK最亮的作品系列,甚至在中国大陆以及港台地区足以超越Capcom的”街头霸王”系列的超高人气。)
该系列独特的画面出自当时被SNK吸收的IREM(刚从街机撤退,代表作为忍者棒球/野球格斗)团队,故爆炸等特效较接近STG。另外,乾净清爽的画风和修长的人物比例佐以时尚的造型,摆脱过往FTG【参战角色几乎都是男性】这一定理,吸引了不少女性玩家。因此,该游戏系列的部分角色拥有极高人气,比如草薙京和其对手八神庵,另外部分女性角色人气甚至比游戏主角都高出很多(拥护者大多数为男性),男性角色与女性角色的周边商品的购买比例为1:2。
该游戏在中国、韩国的人气非常高,在中国,游戏厅对战与网络对战的现象都比日本本国繁荣许多,在日本举办的’斗剧‘比赛上,中、韩两国有着活跃表现的玩家也有不少。
格斗大会
格斗大会KOF,在游戏的设定中,最初是由《饿狼传说》中登场人物——吉斯·霍华德于南镇举行的街头格斗比赛,原本因为街头格斗的传统,几乎没有循环赛,比赛开始的信号和KO宣言的裁判也是没有的,且基本上是在野外等进行。但是,【KOF】系列中的格斗大赛KOF与原作的《饿狼传说》和《龙虎之拳2》设定不同(KOF的设定为’在赞助商支持下,发展成为被各个电视台争相转播的全世界规模的格斗大赛’),于是被设定为平行世界使用KOF这一名称。
因为优胜队伍的巨额奖金并给予格斗家最高荣誉而备受关注的全世界格斗大赛,“平安无事地结束一次也没有”,这个大赛,几乎是为达成某种目的而存在,比如卢卡尔·伯恩斯坦一样黑暗世界的掌权者,OROCHI一族、遥远彼之地一族,或是NESTS这样的秘密结社等。因此决赛前或决赛后,都会在大会主办方或个人提供的地方(比如航空母舰、宇宙空间站的秘密基地等),进行一场不同于格斗大会的死斗。同《饿狼传说》系列不同,大会主办方基斯死亡是被公开报道,而古利查力度死亡则不被世人知道。
如果格斗家本身没有邀请函,则可以让持有邀请函的格斗家邀请自己组队出场,也可以因持有邀请函本人无法参赛而代替出场。另外,从他人处抢夺的邀请函也有效。还有就是因为大会主办方怀有恶意的目的,为了让特定的人参加而把邀请函留下,或者让其无条件的进入决赛或者中途参战等利用邀请函的事情也有。得到出场资格的队伍或个人需要联系KOF运营委员会进行最后的认证。
关于大会的规则,基本上刀剑与大规模杀伤性武器的使用属于犯规。而鞭、棍棒等钝器,以及超能力(火焰炎和冰雪攻击,或精神力量的使用或手刀为刺穿身体之类的)是被认可的。但是耳环炸弹,或会冒火焰一样工艺品的棍棒(比利),巨大的铁球(陈国汉)和锋利的爪子的手甲(蔡),苦无(如月)等,暗器匕首(山崎龙二。小说版中使用的瞬间犯规输了)和小刀,甚至是全身都是强大火力的人造人(MAXIMA)这样的人物也没有特别说明,因此是可以参加的。
KOF系列的基本为团队战,不过,也有正统作品(家用机版)可以选择团体战还是个人战。
效果展示
本文是作者通过对于原版拳皇游戏的学习,通过逆向工厂制作的拳皇的部分功能,适合前端小白进行练手。
游戏展示
代码讲解
文件结构
index.html
index.html这个是游戏的入口,里面写的很简洁,通过,最上面的div盒子里面存放是游戏上方的,游戏角色的血条,和游戏的时间。然后就是通过js新建一个游戏角色的对象。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>拳皇</title> <link rel="stylesheet" href="/static/css/base.css"> <script src="https://cdn.acwing.com/static/jquery/js/jquery-3.3.1.min.js"></script> </head> <body> <div id="kof"> </div> <script type="module"> import {KOF} from '/static/js/base.js'; let kof = new KOF('kof'); </script> </body> </html>
全局的base.js文件
这个全局的base.js文件中存放的就是,游戏的最为基础的信息,比如,游戏的地图,游戏的角色,之类的初始化。
import { GameMap } from '/static/js/game_map/base.js'; import { Kyo } from '../js/player/kyo.js'; class KOF { constructor(id) { this.$kof = $('#' + id); this.game_map = new GameMap(this); /// 初始化游戏对象 这里默认就是创建出两个对手 this.players = [ new Kyo(this, { id: 0, x: 200, y: 0, width: 120, height: 200, color: 'blue', }), new Kyo(this, { id: 1, x: 900, y: 0, width: 120, height: 200, color: 'red', }), ]; } } export { KOF }
AcGameObject
这个base.js文件中存放的是,整个游戏的运行基础,控制整个游戏的运行调控。
let AC_GAME_OBJECTS = [] class AcGameObject{ constructor(){ AC_GAME_OBJECTS.push(this); this.timedela = 0; // 游戏的时间间隔 this.has_call_start = false; // 默认项目没有开始执行 } start(){ // 初始执行一次 } update(){ // 每一帧执行一次(除了第一帧数以外) } destroy(){ // 删除当前对象 for (let i in AC_GAME_OBJECTS){ if (AC_GAME_OBJECTS[i] === this){ AC_GAME_OBJECTS.splice(i, 1); // 删除当前元素 意思是在第i个位置删除1个项目 break; } } } } let last_timestamp; let AC_GAME_OBJECTS_FRAME = (timestamp) => { for (let obj of AC_GAME_OBJECTS){ if (!obj.has_call_start){ // 如果没有执行过 开始执行 obj.start(); obj.has_call_start = true; // 标记这个对象已经执行 } else { obj.timedelta = timestamp - last_timestamp; obj.update(); // 执行过之后就更新 } } last_timestamp = timestamp; requestAnimationFrame(AC_GAME_OBJECTS_FRAME); // 递归 } requestAnimationFrame(AC_GAME_OBJECTS_FRAME); // 函数入口 export{ AcGameObject }
游戏地图的base.js文件
整个base.js文件负责绘制整个游戏的游戏地图,里面主要使用的技术为canvas画布来制作游戏的画布对象,可以根据注释的内容来,自行修改游戏的时间。
import {AcGameObject} from '/static/js/AC_GAME_OBJECT/base.js'; import { Controller } from '../controller/base.js'; export class GameMap extends AcGameObject{ constructor(root){ super(); this.root = root; // 创建背景画布 this.$canvas = $('<canvas width="1280" height="720" tabindex=0></canvas>'); this.ctx = this.$canvas[0].getContext('2d'); this.root.$kof.append(this.$canvas); this.$canvas.focus(); // 给画布控制器 this.controller = new Controller(this.$canvas); this.root.$kof.append($(`<div class="kof-head"> <div class="kof-head-hp-0"><div><div></div></div></div> <div class="kof-head-timer">60</div> <div class="kof-head-hp-1"><div><div></div></div></div> </div>`)); // 设置时间 this.time_left = 60000; // ms this.$timer = this.root.$kof.find(".kof-head-timer"); } start(){ } update(){ // 通过清空背景来刷新 // 控制时间 this.time_left -= this.timedelta; if (this.time_left < 0){ this.time_left = 0; let [a, b] = this.root.players; if (a.status !== 6 && a.status !== 6){ a.status = b.status = 6; // 状态清空 a.frame_current_cnt = b.frame_current_cnt = 0; a.vx = b.vx = 0; } } // 添加时间 this.$timer.text(parseInt(this.time_left / 1000)); this.render(); } render(){ // 清空背景 this.ctx.clearRect(0, 0, this.ctx.canvas.width,this.ctx.canvas.height); } }
Controller的base.js文件
控制图片的生成。
// 控制图片的类 export class Controller { constructor($canvas) { this.$canvas = $canvas; this.pressed_keys = new Set(); // Set对象是单调不重复的 this.start(); } start() { let outer = this; this.$canvas.keydown(function (e) { outer.pressed_keys.add(e.key); }); this.$canvas.keyup(function (e) { outer.pressed_keys.delete(e.key); }); } }
Player中的类
游戏角色的基础类,在这个类中存放的是游戏角色的基础信息,还有游戏角色的各种动作组件,通过构造方法把赋值游戏角色的基础信息,里面再定义大量的方法组件,来调控游戏角色的行动。
import { AcGameObject } from "../ac_game_object/base.js"; export class Player extends AcGameObject{ constructor(root, info){ super(); this.root = root; this.id = info.id; this.x = info.x; this.y = info.y; this.width = info.width; this.height = info.height; this.color = info.color; this.vx = 0; this.vy = 0; this.speedx = 400; // 水平速度 this.speedy = -1700; // 跳跃速度 向上跳的时候速度是负的 this.gravity = 50; // 重力加速度 this.direction = 1; // 定义右边是1 左边是-1 // 获取地图的画布 this.ctx = this.root.game_map.ctx; this.pressed_keys = this.root.game_map.controller.pressed_keys; this.status = 3; // 0:idle 1:向前 2:向后 3:跳跃 4:攻击 5:被打 6:死亡 // 默认是跳下来的 this.animations = new Map(); // 用map存放动作的字符串 更方便 // 帧数记录器 this.frame_current_cnt = 0; // 血量 this.hp = 100; // 默认一百点 // 筛选血条框 // 两个血条 一个红条 一个绿条 this.$hp = this.root.$kof.find(`.kof-head-hp-${this.id}>div`); // 红条 this.$hp_div = this.$hp.find('div'); // 绿条 } start(){ } update_move(){ // 给所有动作加上 重力 但是后面会有特判 this.vy += this.gravity; // 纵轴方向每秒改变一次 this.x += this.vx * this.timedelta / 1000; // 按秒改变x,y方向的位置 this.y += this.vy * this.timedelta / 1000; // // 创建角色碰撞 防止两个角色重叠 // let [a, b] = this.root.players; // if (a !== this) [a, b] = [b, a]; // let r1 = { // x1: a.x, // y1: a.y, // x2: a.x + a.width, // y2: a.y + a.height, // }; // let r2 = { // x1: b.x, // y1: b.y, // x2: b.x + b.width, // y2: b.y + b.height, // }; // // 如果两个矩阵重叠了 制作推人的效果 // if (this.is_collision(r1, r2)){ // // 撞倒之后让对方移动代码 /2是为了让控制速度 // b.x += this.vx * this.timedelta / 1000 / 2; // b.y += this.vy * this.timedelta / 1000 / 2; // // 自己也要减一下 这样才综合 // a.x -= this.vx * this.timedelta / 1000 / 2; // a.y -= this.vy * this.timedelta / 1000 / 2; // if(this.status === 3) this.status = 0; // 跳完了之后变成静止状态 // } if (this.y > 450){ this.y = 450; this.vy = 0; // 只有当 是从跳跃状态到vy变为0的时候 才会 是静止状态 if(this.status === 3){ this.status = 0; // 跳完了之后变成静止状态 } } // 控制边界 if (this.x < 0){ this.x = 0; } else if (this.x + this.width > this.root.game_map.$canvas.width()){ this.x = this.root.game_map.$canvas.width() - this.width; } } // 这里是给游戏角色添加操作 两个角色 update_control(){ let w, a, d, space; if (this.id === 0){ // w是跳,a,d是左右移动,space是攻击 w = this.pressed_keys.has('w'); a = this.pressed_keys.has('a'); d = this.pressed_keys.has('d'); space = this.pressed_keys.has(' '); } else { w = this.pressed_keys.has('ArrowUp'); a = this.pressed_keys.has('ArrowLeft'); d = this.pressed_keys.has('ArrowRight'); space = this.pressed_keys.has('Enter'); } if (this.status === 0 || this.status === 1){ if (space){ // 切换到攻击 this.status = 4; // 停下来 this.vx = 0; // 为了让攻击停下来 this.frame_current_cnt = 0; } else if (w){ // 这个是跳和其他按键组合 if (d){ this.vx = this.speedx; // 保持速度不变 } else if (a){ // 速度换个方向 this.vx = -this.speedx; } else { // 攻击和跳起来的时候x方向速度为0 this.vx = 0; } this.vy = this.speedy; // 跳起来的时候添加一个向上的速度 this.status = 3; // 把动作改成向上的状态 this.frame_current_cnt = 0; } else if (d){ this.vx = this.speedx; // 向右有个速度 this.status = 1; } else if (a){ this.vx = -this.speedx; this.status = 1; } else { this.vx = 0; // 让player没有按的时候可以停止 this.status = 0; } } } // 制作镜像 让交错经过之后 转头 update_direction(){ if(this.status === 6) return; // 逝者安息 倒地之后就不可以转身了 let players = this.root.players; if (players[0] && players[1]){ let me = this, you = players[1 - this.id]; if (me.x < you.x) me.direction = 1; else me.direction = -1; } } // 控制攻击的类 is_attack(){ if (this.status === 6) return; // 逝者安息 防止诈尸 倒地了就不能再被打了 // 攻击到之后 变为被攻击之后的状态 this.status = 5; this.frame_current_cnt = 0; this.hp = Math.max(this.hp - 5, 0); // 被打一下掉5点血 然后位置防止血为负数 所以要Math // 动画的慢慢减血 this.$hp_div.animate({ width: this.$hp.parent().width() * this.hp / 100 // 按百分比减 }, 330); this.$hp.animate({ width: this.$hp.parent().width() * this.hp / 100 // 按百分比减 }, 1000); // 没血了就没了 if (this.hp <= 0){ this.status = 6; this.frame_current_cnt = 0; this.vx = 0; } } // 判断两个举行之间是否有交集的算法 is_collision(r1, r2) { if (Math.max(r1.x1, r2.x1) > Math.min(r1.x2, r2.x2)) return false; if (Math.max(r1.y1, r2.y1) > Math.min(r1.y2, r2.y2)) return false; return true; } // 更新攻击 update_attack(){ // 就是当时攻击的动作的时候 而且 而且攻击这个动作的帧数是18的时候 就算攻击到 if (this.status === 4 && this.frame_current_cnt == 18){ let me = this, you = this.root.players[1 - this.id]; let r1; if (this.direction > 0){ // 定义两个矩形 就是此时我方角色的拳头 和 敌方的这个人这两个矩阵是否有交集 如果有交集算是打到了 r1 = { x1: me.x + 120, y1: me.y + 40, x2: me.x + 120 + 100, y2: me.y + 40 + 20, }; } else { r1 = { x1: me.x + me.width - 120 - 100, y1: me.y + 40, x2: me.x + me.width - 120 - 100 + 100, y2: me.y + 40 + 20 }; } let r2 = { x1: you.x, y1: you.y, x2: you.x + you.width, y2: you.y + you.height }; if (this.is_collision(r1, r2)){ you.is_attack(); } } } update(){ this.update_control(); // 最开始先载入控制器 this.update_move(); // 然后再计算好当前这一帧的状态是什么 this.update_direction(); this.update_attack(); this.render(); // 然后清除当前状态 } render(){ // 寻找攻击位置的代码 // // 人物的图像信息 // this.ctx.fillStyle = "blue"; // this.ctx.fillRect(this.x, this.y, this.width,this.height); // // 画一个小方块 攻击的位置 // if (this.direction > 0){ // this.ctx.fillStyle = 'red'; // this.ctx.fillRect(this.x + 120, this.y + 40, 100, 20); // } else { // this.ctx.fillStyle = 'red'; // this.ctx.fillRect(this.x + this.width - 120 - 100, this.y + 40, 100, 20); // } let status = this.status; // 载入状态 // 如果是向前的而且 速度方向为负数了 那么就是后退 if (this.status === 1 && this.direction * this.vx < 0) status = 2; let obj = this.animations.get(status); // 载入每个动画状态2 // 如果已经被加载出来了 if (obj && obj.loaded){ // 实现镜像 if (this.direction > 0){ // 循环渲染 // 这里面的除以obj.frame_rate 就是用来控制速度的 let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt; let image = obj.gif.frames[k].image; this.ctx.drawImage(image, this.x, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale); // 这里长宽 * obj.scale 是用来放大图片的 } else { this.ctx.save(); this.ctx.scale(-1, 1); this.ctx.translate(-this.root.game_map.$canvas.width(), 0); // 循环渲染 // 这里面的除以obj.frame_rate 就是用来控制速度的 let k = parseInt(this.frame_current_cnt / obj.frame_rate) % obj.frame_cnt; let image = obj.gif.frames[k].image; this.ctx.drawImage(image, this.root.game_map.$canvas.width() - this.x - this.width, this.y + obj.offset_y, image.width * obj.scale, image.height * obj.scale); // 这里长宽 * obj.scale 是用来放大图片的 this.ctx.restore(); } } // 对攻击动作的特判 攻击动作与被攻击都会被特判 if (status === 4 || status === 5 || this.status === 6){ // 攻击动作播放完倒数第二帧之后 就可以停 这样可以防止闪一下 更丝滑 if (this.frame_current_cnt === obj.frame_rate * (obj.frame_cnt - 1)){ // 特判一下 如果倒地了 就回到上一帧 不会重新起来了 if (this.status === 6){ this.frame_current_cnt --; } else { this.status = 0; } } } this.frame_current_cnt ++; // 改变帧数 这样图片才会一直动 // 默认一秒60次 有点快了 // 可以通过frame_rate 调节 } }
上面是游戏的角色的基础信息文件,然后我们想要具体的实现一个角色的类,那么就需要通过继承的方式,来实现这个游戏角色的具体信息。
import { Player } from "./base.js"; import { GIF } from "../utils/gif.js"; export class Kyo extends Player{ constructor(root, info){ super(root, info); this.init_animations(); // 初始化动画 } init_animations(){ let outer = this; // 这里的两个-22是左右行走的时候的偏移量 防止往下走的的太厉害 // 跳起来的时候也要高一些 let offsets= [0, -22, -22, -140, 0, 0, 0]; for (let i = 0; i < 7; i++) { let gif = GIF(); gif.load(`/static/images/player/kyo/${i}.gif`); this.animations.set(i, { gif: gif, frame_cnt: 0, // 总图片数 frame_rate: 5, // 每5帧过度一次 offset_y: offsets[i], // y方向偏移量 loaded: false, // 是否加载完整 scale: 2, // 放大多少倍 }); // 加载gif图 gif.onload = function () { let obj = outer.animations.get(i); obj.frame_cnt = gif.frames.length; obj.loaded = true; if (obj.status === 3){ obj.frame_rate = 4; } } } } }
完整的游戏地址为:https://gitee.com/geek-li-hua/boxing-emperor.git
如果大家觉得有用的话,可以关注我下面的微信公众号,极客李华,我会在里面更新更多行业资讯,企业面试内容,编程资源,如何写出可以让大厂面试官眼前一亮的简历等内容,让大家更好学习编程,我的抖音,B站也叫极客李华。大家喜欢也可以关注一下