基于Cocos Creator 2.4.x、Socket.io、TypeScript实现的ddz游戏
实现了加入房间、抢地主、出牌、牌型比较、重新开始等功能。
服务端:https://gitee.com/baymax668/koa-ddz-server
script目录结构:
|-- config # 【配置】
| |-- ServerConfig.ts # 连接服务器IP和端口配置
|-- constant # 【常量/枚举】
| |-- GameOption.ts
| |-- GameState.ts # 游戏阶段/状态
| |-- PokerHand.ts # 牌型
| |-- PokerPoints.ts # 牌点数
| |-- PokerSuits.ts # 牌花色
| |-- SocketGameEvent.ts # socket游戏相关事件枚举
| |-- SocketRoomEvent.ts # socket房间相关事件枚举
|-- controllers # 【控制类】会被挂载到预制体根节点上
| |-- CardCtrl.ts # 卡牌预制体控制类
| |-- LandCardsCtrl.ts # 地主牌预制体控制类
| |-- PlayerCtrl.ts # 局内玩家数据预制体控制类
| |-- TimerCtrl.ts # 定时器预制体控制类
|-- data #【模型/数据对象】
| |-- Player.ts # 玩家模型
| |-- Poker.ts # 卡牌模型
| |-- ResponseData.ts # http请求数据模型
| |-- Room.ts # 房间模型
| |-- SocketData.ts # socket数据模型
| |-- User.ts # 用户模型
|-- managers #【管理】
| |-- NetManager
| | |-- NetMgr.ts # Http Api请求封装
| |-- SceneManager # 场景管理类,会被挂载到对应的场景根节点上
| | |-- HallMgr.ts # 大厅场景管理类
| | |-- LoginMgr.ts # 登录场景管理类
| | |-- RoomMgr.ts # 房间场景管理类(*)
|-- store
| |-- Store.ts # 全局数据
|-- utils #【工具类】
| |-- HttpUtil.ts # Http Ajax封装,基于Promise封装Get Post
| |-- PokerUtil.ts # 牌型校验工具类
| |-- RandomUtil.ts # 随机函数工具类
说明:本机玩家的座位永远在在屏幕的下方(1号座位),需要根据返回的房间玩家数组,推算上家下家座位(2号座位、3号座位),并且正确显示
思路:
座位号函数:
系统通过Socket推送数据时,不会提供下标和座位号,需要封装一个函数,推算某个玩家的座位号
说明:本机玩家手牌以横线排列格式显示,并且获取新牌或减少牌时能动态排列格式
思路:
说明:卡牌需要重复使用,并且要设定点击事件返回获取对应的卡牌数据,故封装成一个Prefab
思路:
public init(poker:Poker)
函数,用于初始化卡牌数据和显示的资源public addTouchListener(callback)
添加点击事件,并给回调函数返回内部poker对象核心代码:
import { PokerPoints } from "../constant/PokerPoints";
import { Poker } from "../data/Poker";
const { ccclass, property } = cc._decorator;
export type CardTouchCallback = (poker: Poker, isSelected: boolean) => void
/**
* 卡牌控制类
* 会被挂载到card预制体上
* 实现触摸上升,和触摸事件通知
*/
@ccclass
export default class CardCtrl extends cc.Component {
@property(cc.SpriteAtlas)
cardSpriteAtlas: cc.SpriteAtlas = null
public poker: Poker = null
private isSelected = false
private touchListeners: Set<Function | CardTouchCallback> = new Set()
public init(poker?: Poker): void {
this.poker = poker
this.initTouchEvent()
if (this.poker) {
this.show()
}
}
/**
* 初始化卡牌触摸事件
*/
private initTouchEvent() {
this.node.on(cc.Node.EventType.TOUCH_START, this.onTouchEvent.bind(this))
}
/**
* 触摸事件
* 设置卡片上升或下降
*/
private onTouchEvent() {
const distance = 20
// 上升或下降动画
if (this.isSelected) {
// 下降
cc.tween(this.node)
.to(0.1, { y: 0 })
.start()
} else {
// 上升
cc.tween(this.node)
.to(0.1, { y: distance })
.start()
}
this.isSelected = !this.isSelected
// 触发所有监听器
this.touchListeners.forEach(callback => {
callback(this.poker, this.isSelected)
})
}
// 添加点击触摸事件
public addTouchListener(callback: Function | CardTouchCallback): Function {
this.touchListeners.add(callback)
return callback
}
// ...
}
说明:场景之间需要共有一些数据,Cocos默认没有场景传参,可以设定一个全局变量实现场景传参
思路:设定一个Store类,内部定义全局变量,任意脚本需要使用时引用该类进行使用(变量放在类中可以避免全局变量污染)
// Store.ts
import { IRoomVo } from "../data/Room"
import { IUserVo } from "../data/User"
/**
* 全局数据
*/
export default class Store {
// 用户数据
public static user: IUserVo
// 房间数据
public static room: IRoomVo
public static index: number // 本机玩家在房间的座位号
private constructor() { }
}
说明:牌型校验会在客户端校验一次,并且进行比较大小,这一步会防止客户端给服务端发送无效牌组,减少服务器校验错误率。服务端为了安全还会再校验一次,防止网络数据篡改。
校验工具类:PokerUtil.ts
坑:this.schedule
在浏览器中离开页面后会停止定时,所有需要使用setInterval
进行定时
定时思路:使用setInterval
定时,每秒执行回调函数,并且对定时变量减1,当定时变量小于0时,关闭定时器
关键代码:
// TimerCtrl.ts
const { ccclass, property } = cc._decorator;
@ccclass
export default class TimerCtrl extends cc.Component {
private _second: number = 0
private callbacks: Set<Function> = new Set()
private interval = null
public init(second: number) {
this._second = second
this.callbacks = new Set()
this.setViewTimeNum(this._second)
// 启动定时
this.interval = setInterval(this.timeCallback.bind(this), 1000);
}
private timeCallback() {
this._second--
// 触发超时回调
if (this._second < 0) {
clearInterval(this.interval) // 关闭定定时
console.log('run callback')
this.callbacks.forEach(callback => {
callback()
})
} else {
this.setViewTimeNum(this._second)
}
}
}
说明:对Xhr使用Promise进行封装
核心代码:
// HttpUtil.ts
/**
* HTTP AJAX请求封装
*/
export default class HttpUtil {
public static baseUrl: string = 'http://localhost:3000'
public static timeout: number = 3000
/**
* HTTP Get请求
* @param url
* @param query
* @returns
*/
public static get(url: string, query?: object): Promise<unknown> {
const _url = this.baseUrl + url
const xhr = new XMLHttpRequest()
// 参数解析
let queryStr = ''
if (query) {
queryStr = this.queryToString(query)
}
const promise = new Promise((resolve, reject) => {
// init xhr
xhr.open('GET', _url + queryStr)
xhr.timeout = this.timeout
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// xhr.setRequestHeader("Content-Type", "application/JSON");
// 响应处理
xhr.onreadystatechange = () => {
if (xhr.readyState != 4) { return }
let response: any = xhr.responseText
// 尝试变为JSON数据
try {
response = JSON.parse(xhr.responseText)
} catch { }
if (xhr.status >= 200 && xhr.status < 400) {
resolve(response)
} else {
console.error(`post request error\nstatus:${xhr.status}, url:${_url}\nresponse:`, response)
reject()
}
}
xhr.send()
})
return promise
}
/**
* Http Post请求
* @param url
* @param params
*/
public static post(url: string, params?: object): Promise<unknown> {
const _url = this.baseUrl + url
const xhr = new XMLHttpRequest()
const promise = new Promise((resolve, reject) => {
// init xhr
xhr.open('POST', _url)
xhr.timeout = this.timeout
xhr.setRequestHeader("Content-Type", "application/JSON"); // JSON格式
// 响应处理
xhr.onreadystatechange = () => {
if (xhr.readyState != 4) { return }
let response: any = xhr.responseText
// 尝试变为JSON数据
try {
response = JSON.parse(xhr.responseText)
} catch { }
if (xhr.status >= 200 && xhr.status < 400) {
resolve(response)
}
else {
console.error(`post request error\nstatus:${xhr.status}, url:${_url}\nresponse:`, response)
reject()
}
}
xhr.send(JSON.stringify(params))
})
return promise
}
/**
* 对象参数转成get请求的参数形式
* @param query
* @returns
*/
private static queryToString(query: object): string {
let queryStr = ''
for (const key in query) {
if (typeof query[key] !== "object") {
queryStr += `${key}=${query[key]}`
}
}
if (queryStr == '') {
return queryStr
}
return '?' + queryStr
}
}
Solitaire: 纸牌游戏-学习Cocos项目 (gitee.com)
tinyshu/ddz_game: 斗地主游戏 (github.com)
功能设计与原型图:./doc/游戏功能与原型设计.md
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。