1 Star 1 Fork 3

Baymax / cocos-ddz-client

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

介绍

基于Cocos Creator 2.4.x、Socket.io、TypeScript实现的ddz游戏

实现了加入房间、抢地主、出牌、牌型比较、重新开始等功能。

服务端https://gitee.com/baymax668/koa-ddz-server

image-20230524211318075

image-20230524211549976

目录结构

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号座位),并且正确显示

image-20230527215722439

思路

  1. 先找到本机玩家的在玩家数组中的下标
  2. 根据本机玩家下标,往右遍历,第一个就是2号位置,第二个就是3号位置(超过数组长度时,需要归零)

座位号函数

系统通过Socket推送数据时,不会提供下标和座位号,需要封装一个函数,推算某个玩家的座位号

  1. 定义getIndexToSeatNum(index:number)函数,根据玩家下标返回对应的座位号
  2. 记录本机玩家的下标,并且根据这个下标为起点(座位号1)往后遍历,找到某名玩家号即可推算出座位号

手牌显示

说明:本机玩家手牌以横线排列格式显示,并且获取新牌或减少牌时能动态排列格式

image-20230527221314076

思路

  1. 设定一个节点,用于存放所有手牌v
  2. 添加Layout组件,设定Type自动布局为Horizontal横向排列;ResizeModeContainer缩放节点,实现整个牌组居中;设定SpacingX属性,让手牌之间实现层叠显示

image-20230527221539929

卡牌预制体

说明:卡牌需要重复使用,并且要设定点击事件返回获取对应的卡牌数据,故封装成一个Prefab

思路

  1. 创建一个Prefab,设置public init(poker:Poker)函数,用于初始化卡牌数据和显示的资源
  2. 提供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)
        }
    }
}

Ajax封装

说明:对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

空文件

简介

基于Cocos Creator2.x、TypeScript、Socket.io实现的ddz游戏客户端 展开 收起
TypeScript
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
1
https://gitee.com/baymax668/cocos-ddz-client.git
git@gitee.com:baymax668/cocos-ddz-client.git
baymax668
cocos-ddz-client
cocos-ddz-client
master

搜索帮助