目录
HTML+JS+websocket 实例,联机“游戏王”对战 1
HTML+JS+websocket 实例,联机“游戏王”对战 2 - 联机模式
HTML+JS+websocket 实例,联机“游戏王”对战 3 - 界面布局
HTML+JS+websocket 实例,联机“游戏王”对战 4 - 卡组系统
HTML+JS+websocket 实例,联机“游戏王”对战 5 - 卡片选中系统
HTML+JS+websocket 实例,联机“游戏王”对战 6 - 卡片放置,战场更新
HTML+JS+websocket 实例,联机“游戏王”对战 7 - 墓地,副控制面板
HTML+JS+websocket 实例,联机“游戏王”对战 8 - 返回手卡,卡组
HTML+JS+websocket 实例,联机“游戏王”对战 9 - 实现简单 websocket 通信
HTML+JS+websocket 实例,联机“游戏王”对战 10 - 搭建游戏服务端
HTML+JS+websocket 实例,联机“游戏王”对战 11 - 客户端消息的收发
HTML+JS+websocket 实例,联机“游戏王”对战 12 - 消息发送具体场景
HTML+JS+websocket 实例,联机“游戏王”对战 13 - 实机演示
这章开始要上一些代码了,没什么优化也比较偷懒,见谅。
界面布局
先来看看游戏界面的整体布局,完整HTML代码放在最后了:
这里的HTML代码非常冗余,以后会尝试引入PHP+面向对象的方法简化代码结构。另外游戏界面几乎所有元素的 position 属性都偷懒统一使用 absolute 且固定长宽(部分采用百分比,具体可以查看项目的css文件)。这意味着整个游戏界面的大小,每个区域的位置或是每个区域之间的相对位置,都不会随着浏览器的窗口大小的变动而变动。也就是说,如果你在小屏电脑(或缩小的浏览器窗口)上打开该游戏就会是下面这个样子:
当然用浏览器缩放一下还是能解决这个问题的。
冗余结构非常不便于属性及参数的修改,一块地方改动可能导致其他地方全要改,好在目前布局已经定好了(如果不考虑更加复杂的功能扩展),而且似乎不影响讲解。接下来我们还是分区域逐一介绍下游戏界面。
首先我们可以打开浏览器的“元素审查”功能,这样可以帮助我们更快捷的了解游戏的整体布局:
可以看到,游戏的主体大致分为几个区域:
1.主控制面板(control-field):
<!-- 主控制面板 -->
<div class="control-field colm-controlfield height-total clearf">
<div class="card-field">
<img id="card-info" class="card" src="">
</div>
<div class="option-button">
<button class="button" type="button" name="attkSummon" onclick="placeCard('attk', 'monster')">攻击召唤</button>
<button class="button" type="button" name="defenSummon" onclick="placeCard('defen', 'monster')">守备召唤</button>
<button class="button" type="button" name="backSummon" onclick="placeCard('back', 'monster')">背盖召唤</button>
<button class="button" type="button" name="changeState" onclick="changeState('monster')">更变形式</button>
<button class="button" type="button" name="launchCard" onclick="placeCard('on', 'magic')">发动(手卡)</button>
<button class="button" type="button" name="coverCard" onclick="placeCard('off', 'magic')">覆盖(手卡)</button>
<button class="button" type="button" name="openCard" onclick="changeState('magic')">打开盖卡</button>
<button class="button" type="button" name="backtoHand" onclick="backtoHand()">回到手卡</button>
<button class="button" type="button" name="backtoDeck" onclick="backtoDeck()">回到卡组</button>
<button class="button" type="button" name="sendtoTomb" onclick="sendtoTomb()">送去墓地</button>
<button class="button" type="button" name="sendtoGameout" onclick="sendtoGameout()">除去游戏外</button>
<button class="button" type="button" name="selectGameout" onclick="selectGameout()">从游戏外选择</button>
</div>
</div>
主控面板主要分为上下两块,上块是用于展现卡片的详细信息,其实就是放大的图片,尽量用高清图片,如果手动录入卡片信息工程量会非常大,真有需要的话可以尝试图片OCR,印刷字体会很好识别,但是一般OCR能识别的文字人眼也可以看清了,所以这里我们直接采用放大的图片。
卡片信息展示区的触发条件是鼠标浮悬或点击了某个存在卡牌的 img 标签,具体我们放到 js 的章节讲。下块区域是各种操作按钮,type 都是 button,onclick 属性设置对应的触发函数,具体函数逻辑也放到 js 里讲。
2. 主要场地(main-field):
代码贴一半吧,太冗余了,基本是对称的:
<div class="hand-field colm-10 height-2">
<div class="card-field"> <!-- 对方手卡置卡区(上限8张) -->
<div class="item">
<img id="p2-hand7" class="card" onmouseover="showCardInfo('hand', this.src, 7, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand6" class="card" onmouseover="showCardInfo('hand', this.src, 6, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand5" class="card" onmouseover="showCardInfo('hand', this.src, 5, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand4" class="card" onmouseover="showCardInfo('hand', this.src, 4, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand3" class="card" onmouseover="showCardInfo('hand', this.src, 3, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand2" class="card" onmouseover="showCardInfo('hand', this.src, 2, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand1" class="card" onmouseover="showCardInfo('hand', this.src, 1, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand0" class="card" onmouseover="showCardInfo('hand', this.src, 0, 'player2')" src="">
</div>
</div>
</div>
<div class="battle-field colm-10 height-6">
<div class="card-field"> <!-- 对方战场卡牌区域 -->
<div id="p2field8" class="item"> <!-- 怪兽区(上限5张) -->
<img id="p2-field8" class="card" onmouseover="showCardInfo('field', this.src, 8, 'player2')" onclick="selectCard('p2field8', 'field', this.src, 8, 'player2')" src="">
</div>
<div id="p2field6" class="item">
<img id="p2-field6" class="card" onmouseover="showCardInfo('field', this.src, 6, 'player2')" onclick="selectCard('p2field6', 'field', this.src, 6, 'player2')" src="">
</div>
<div id="p2field5" class="item">
<img id="p2-field5" class="card" onmouseover="showCardInfo('field', this.src, 5, 'player2')" onclick="selectCard('p2field5', 'field', this.src, 5, 'player2')" src="">
</div>
<div id="p2field7" class="item">
<img id="p2-field7" class="card" onmouseover="showCardInfo('field', this.src, 7, 'player2')" onclick="selectCard('p2field7', 'field', this.src, 7, 'player2')" src="">
</div>
<div id="p2field9" class="item">
<img id="p2-field9" class="card" onmouseover="showCardInfo('field', this.src, 9, 'player2')" onclick="selectCard('p2field9', 'field', this.src, 9, 'player2')" src="">
</div>
<div id="p2field3" class="item"> <!-- 魔法陷阱区(上限5张) -->
<img id="p2-field3" class="card" onmouseover="showCardInfo('field', this.src, 3, 'player2')" onclick="selectCard('p2field3', 'field', this.src, 3, 'player2')" src="">
</div>
<div id="p2field1" class="item">
<img id="p2-field1" class="card" onmouseover="showCardInfo('field', this.src, 1, 'player2')" onclick="selectCard('p2field1', 'field', this.src, 1, 'player2')" src="">
</div>
<div id="p2field0" class="item">
<img id="p2-field0" class="card" onmouseover="showCardInfo('field', this.src, 0, 'player2')" onclick="selectCard('p2field0', 'field', this.src, 0, 'player2')" src="">
</div>
<div id="p2field2" class="item">
<img id="p2-field2" class="card" onmouseover="showCardInfo('field', this.src, 2, 'player2')" onclick="selectCard('p2field2', 'field', this.src, 2, 'player2')" src="">
</div>
<div id="p2field4" class="item">
<img id="p2-field4" class="card" onmouseover="showCardInfo('field', this.src, 4, 'player2')" onclick="selectCard('p2field4', 'field', this.src, 4, 'player2')" src="">
</div>
</div>
主要场地分为我方/对方的手牌区域与战场区域,每块区域间有数个独立的 item(手牌8个,战场10个),每个 item 里是独立的 img 标签。img 标签的id将手牌与战场区分开来,手牌id格式 p1-handx/p2-handx;p1代表我方,p2代表对方(之后其他代码里P1均代表我方,P2均代表对方),x代表手牌序号,上限是8张;战场id同理,上限是10张。注意这里的战场id我并非按照0~9的顺序排下来,而是按8,6,5,7,9,3,1,0,2,4(对方战场);3,1,0,2,4,8,6,5,7,9(我方战场)这样排序,目的是为了使卡片向战场放置的时候从最中间的那个卡槽开始放置,之后分别是左右两个卡槽,再到最外边左右两个卡槽,怪兽区,魔法陷阱区均遵守这个放置规则,这样游戏体验会好一点。
手牌与战场区的img标签均设置 mouseover 触发卡片信息展示,onclick 触发卡片选择函数。注意对方手卡卡槽没有设置 onclick 函数,因为我们规定玩家无法直接对对手手卡进行操作,如果碰上手卡交换类的卡片效果可以把需要交换的卡牌放置到场上再各自拿到手牌;如果有效果要丢弃对手的手牌可以在告知对手后由对手完成丢弃指定手牌的操作。实现游戏的时候我最终还是决定不为这些不常用的功能徒增函数的复杂度,能通过基本功能或玩家间交流实现的操作就暂不额外添加对应功能。
3. 卡组,环境卡槽区(rside-field):
<!-- 卡组,环境卡槽区 -->
<div class="rside-field colm-deckfield height-total clearf">
<div class="item env">
<img id="p1-env" class="card" onmouseover="" src="">
</div>
<div class="item deck">
<img id="deck_r" class="card" src="image/cards/cardback.jpg" alt="cardback" onclick="drawCard()">
</div>
</div>
这块区域最为简单,一个普通的卡槽与一个显示卡背的卡槽,显示卡背的卡槽表示卡组,onclick 设置抽卡函数,点击抽卡。
4. 副控制面板(card-selection):
注:在 js 代码里我把这一块区域在备注里定义为“sub-field“,html里命名暂未修改,命名这块一直很纠结。
<!-- 副控制面板 -->
<div class="card-selection">
<div id="select-area" class="selection-area"></div>
<div class="button-area">
<button class="button" type="button" onclick="sf_buttons('deck')">从牌组中选择(刷新列表)</button>
<button class="button" type="button" onclick="sf_buttons('p1tomb')">从我方墓地选择(刷新列表)</button>
<button class="button" type="button" onclick="sf_buttons('p2tomb')">从对方墓地选择(刷新列表)</button>
<button class="button" type="button" onclick="shuffleDeck()">洗牌</button>
</div>
</div>
副控制面板也分为上下两个区域,上侧区域会根据下方的按钮展示不同的内容(除洗牌按钮外)。展示效果是这样的:
例图中显示的是点击“从牌组中选择”按钮后呈现的效果,展示区域会刷新并加载目前卡组中存在的卡牌,卡牌太多显示不下则会出现滚动条,每张卡牌均可被点击进入选中状态,然后可通过其他按钮执行对选中卡牌的相应操作,比如将此卡加入手牌等。具体实现方法我们放到后面结合js一起讲。
整体的游戏界面布局大概就是这样了,关于具体的元素样式如大小尺寸,内外边距,区域透明度,边框颜色等可以直接查看项目的 css 代码。接下来的章节就结合 js 代码谈谈游戏逻辑的设计与实现。
最后附完整html代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.bootcss.com/normalize/8.0.0/normalize.min.css">
<link rel="stylesheet" href="css/ygo-main.css">
<link rel="stylesheet" href="css/ygo-basic.css">
<script type="text/javascript" src="js/game-deck.js"></script>
<script type="text/javascript" src="js/game-cardselection.js"></script>
<script type="text/javascript" src="js/game-field.js"></script>
<script type="text/javascript" src="js/game-hand.js"></script>
<script type="text/javascript" src="js/game-tomb.js"></script>
<script type="text/javascript" src="js/game-control.js"></script>
<title>CC-游戏王卡牌决斗</title>
</head>
<body>
<style>
body {
width: 1820px;
background-image: url(image/background/background.jpg);
}
</style>
<div class="header clearf"></div>
<!-- 主控制面板 -->
<div class="control-field colm-controlfield height-total clearf">
<div class="card-field">
<img id="card-info" class="card" src="">
</div>
<div class="option-button">
<button class="button" type="button" name="attkSummon" onclick="placeCard('attk', 'monster')">攻击召唤</button>
<button class="button" type="button" name="defenSummon" onclick="placeCard('defen', 'monster')">守备召唤</button>
<button class="button" type="button" name="backSummon" onclick="placeCard('back', 'monster')">背盖召唤</button>
<button class="button" type="button" name="changeState" onclick="changeState('monster')">更变形式</button>
<button class="button" type="button" name="launchCard" onclick="placeCard('on', 'magic')">发动(手卡)</button>
<button class="button" type="button" name="coverCard" onclick="placeCard('off', 'magic')">覆盖(手卡)</button>
<button class="button" type="button" name="openCard" onclick="changeState('magic')">打开盖卡</button>
<button class="button" type="button" name="backtoHand" onclick="backtoHand()">回到手卡</button>
<button class="button" type="button" name="backtoDeck" onclick="backtoDeck()">回到卡组</button>
<button class="button" type="button" name="sendtoTomb" onclick="sendtoTomb()">送去墓地</button>
<button class="button" type="button" name="sendtoGameout" onclick="sendtoGameout()">除去游戏外</button>
<button class="button" type="button" name="selectGameout" onclick="selectGameout()">从游戏外选择</button>
</div>
</div>
<!-- 游戏主要场地 -->
<div class="main-field colm-mainfield height-total clearf">
<div class="hand-field colm-10 height-2">
<div class="card-field"> <!-- 对方手卡置卡区(上限8张) -->
<div class="item">
<img id="p2-hand7" class="card" onmouseover="showCardInfo('hand', this.src, 7, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand6" class="card" onmouseover="showCardInfo('hand', this.src, 6, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand5" class="card" onmouseover="showCardInfo('hand', this.src, 5, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand4" class="card" onmouseover="showCardInfo('hand', this.src, 4, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand3" class="card" onmouseover="showCardInfo('hand', this.src, 3, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand2" class="card" onmouseover="showCardInfo('hand', this.src, 2, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand1" class="card" onmouseover="showCardInfo('hand', this.src, 1, 'player2')" src="">
</div>
<div class="item">
<img id="p2-hand0" class="card" onmouseover="showCardInfo('hand', this.src, 0, 'player2')" src="">
</div>
</div>
</div>
<div class="battle-field colm-10 height-6">
<div class="card-field"> <!-- 对方战场卡牌区域 -->
<div id="p2field8" class="item"> <!-- 怪兽区(上限5张) -->
<img id="p2-field8" class="card" onmouseover="showCardInfo('field', this.src, 8, 'player2')" onclick="selectCard('p2field8', 'field', this.src, 8, 'player2')" src="">
</div>
<div id="p2field6" class="item">
<img id="p2-field6" class="card" onmouseover="showCardInfo('field', this.src, 6, 'player2')" onclick="selectCard('p2field6', 'field', this.src, 6, 'player2')" src="">
</div>
<div id="p2field5" class="item">
<img id="p2-field5" class="card" onmouseover="showCardInfo('field', this.src, 5, 'player2')" onclick="selectCard('p2field5', 'field', this.src, 5, 'player2')" src="">
</div>
<div id="p2field7" class="item">
<img id="p2-field7" class="card" onmouseover="showCardInfo('field', this.src, 7, 'player2')" onclick="selectCard('p2field7', 'field', this.src, 7, 'player2')" src="">
</div>
<div id="p2field9" class="item">
<img id="p2-field9" class="card" onmouseover="showCardInfo('field', this.src, 9, 'player2')" onclick="selectCard('p2field9', 'field', this.src, 9, 'player2')" src="">
</div>
<div id="p2field3" class="item"> <!-- 魔法陷阱区(上限5张) -->
<img id="p2-field3" class="card" onmouseover="showCardInfo('field', this.src, 3, 'player2')" onclick="selectCard('p2field3', 'field', this.src, 3, 'player2')" src="">
</div>
<div id="p2field1" class="item">
<img id="p2-field1" class="card" onmouseover="showCardInfo('field', this.src, 1, 'player2')" onclick="selectCard('p2field1', 'field', this.src, 1, 'player2')" src="">
</div>
<div id="p2field0" class="item">
<img id="p2-field0" class="card" onmouseover="showCardInfo('field', this.src, 0, 'player2')" onclick="selectCard('p2field0', 'field', this.src, 0, 'player2')" src="">
</div>
<div id="p2field2" class="item">
<img id="p2-field2" class="card" onmouseover="showCardInfo('field', this.src, 2, 'player2')" onclick="selectCard('p2field2', 'field', this.src, 2, 'player2')" src="">
</div>
<div id="p2field4" class="item">
<img id="p2-field4" class="card" onmouseover="showCardInfo('field', this.src, 4, 'player2')" onclick="selectCard('p2field4', 'field', this.src, 4, 'player2')" src="">
</div>
</div>
<div class="card-field"> <!-- 我方战场卡牌区域 -->
<div id="p1field3" class="item"> <!-- 怪兽区(上限5张) -->
<img id="p1-field3" class="card" onmouseover="showCardInfo('field', this.src, 3, 'player1')" onclick="selectCard('p1field3', 'field', this.src, 3, 'player1')" src="">
</div>
<div id="p1field1" class="item">
<img id="p1-field1" class="card" onmouseover="showCardInfo('field', this.src, 1, 'player1')" onclick="selectCard('p1field1', 'field', this.src, 1, 'player1')" src="">
</div>
<div id="p1field0" class="item">
<img id="p1-field0" class="card" onmouseover="showCardInfo('field', this.src, 0, 'player1')" onclick="selectCard('p1field0', 'field', this.src, 0, 'player1')" src="">
</div>
<div id="p1field2" class="item">
<img id="p1-field2" class="card" onmouseover="showCardInfo('field', this.src, 2, 'player1')" onclick="selectCard('p1field2', 'field', this.src, 2, 'player1')" src="">
</div>
<div id="p1field4" class="item">
<img id="p1-field4" class="card" onmouseover="showCardInfo('field', this.src, 4, 'player1')" onclick="selectCard('p1field4', 'field', this.src, 4, 'player1')" src="">
</div>
<div id="p1field8" class="item"> <!-- 魔法陷阱区(上限5张) -->
<img id="p1-field8" class="card" onmouseover="showCardInfo('field', this.src, 8, 'player1')" onclick="selectCard('p1field8', 'field', this.src, 8, 'player1')" src="">
</div>
<div id="p1field6" class="item">
<img id="p1-field6" class="card" onmouseover="showCardInfo('field', this.src, 6, 'player1')" onclick="selectCard('p1field6', 'field', this.src, 6, 'player1')" src="">
</div>
<div id="p1field5" class="item">
<img id="p1-field5" class="card" onmouseover="showCardInfo('field', this.src, 5, 'player1')" onclick="selectCard('p1field5', 'field', this.src, 5, 'player1')" src="">
</div>
<div id="p1field7" class="item">
<img id="p1-field7" class="card" onmouseover="showCardInfo('field', this.src, 7, 'player1')" onclick="selectCard('p1field7', 'field', this.src, 7, 'player1')" src="">
</div>
<div id="p1field9" class="item">
<img id="p1-field9" class="card" onmouseover="showCardInfo('field', this.src, 9, 'player1')" onclick="selectCard('p1field9', 'field', this.src, 9, 'player1')" src="">
</div>
</div>
</div>
<div class="hand-field colm-10 height-2">
<div class="card-field"> <!-- 我方手卡置卡区(上限8张) -->
<div class="item">
<img id="p1-hand0" class="card" onmouseover="showCardInfo('hand', this.src, 0, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 0, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand1" class="card" onmouseover="showCardInfo('hand', this.src, 1, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 1, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand2" class="card" onmouseover="showCardInfo('hand', this.src, 2, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 2, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand3" class="card" onmouseover="showCardInfo('hand', this.src, 3, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 3, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand4" class="card" onmouseover="showCardInfo('hand', this.src, 4, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 4, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand5" class="card" onmouseover="showCardInfo('hand', this.src, 5, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 5, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand6" class="card" onmouseover="showCardInfo('hand', this.src, 6, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 6, 'player1')" src="">
</div>
<div class="item">
<img id="p1-hand7" class="card" onmouseover="showCardInfo('hand', this.src, 7, 'player1')" onclick="selectCard(this.id, 'hand', this.src, 7, 'player1')" src="">
</div>
</div>
</div>
</div>
<!-- 卡组,环境卡槽区 -->
<div class="rside-field colm-deckfield height-total clearf">
<div class="item env">
<img id="p1-env" class="card" onmouseover="" src="">
</div>
<div class="item deck">
<img id="deck_r" class="card" src="image/cards/cardback.jpg" alt="cardback" onclick="drawCard()">
</div>
</div>
<!-- 副控制面板 -->
<div class="card-selection">
<div id="select-area" class="selection-area"></div>
<div class="button-area">
<button class="button" type="button" onclick="sf_buttons('deck')">从牌组中选择(刷新列表)</button>
<button class="button" type="button" onclick="sf_buttons('p1tomb')">从我方墓地选择(刷新列表)</button>
<button class="button" type="button" onclick="sf_buttons('p2tomb')">从对方墓地选择(刷新列表)</button>
<button class="button" type="button" onclick="shuffleDeck()">洗牌</button>
</div>
</div>
</body>
</html>