/BNB

2016~2017学年秋季学期面向对象程序设计课程项目:泡泡堂

Primary LanguageC++

BNB

C++大项目设计


参与人员及分工

  • zwz

负责游戏架构和网络通信
工作量:40%

  • lxz

负责UI设计
工作量:30%

  • lck

负责视觉效果设计
工作量:15%

  • hhy

负责地图和音乐设计
工作量:15%

实现的功能

  • 基础功能

    • 支持地图绘制、人物绘制、水泡效果绘制等
    • 支持鼠标和键盘操作交互
    • 支持障碍物
    • 支持泡泡的放置与爆炸
    • 支持三种基本增强型道具(鞋子,泡泡,药水)
    • 实现服务端:支持局域网联机对战(自由对抗模式),且支持多人在同一个地图游戏
    • 支持动画效果
  • 拓展功能

    • 支持房间列表
    • 支持 >= 2 张地图
    • 临终礼物

用到的C++特性

  • 基础特性

    • STL 容器,如 std::vector
    • 迭代器
    • 类和多态
  • C++11特性

    • 初始化列表
    • 类型推断
    • Lambda 表达式

游戏系统介绍

游戏架构

  • 地图

    • 图形化描述
    graph TB
    A[地图]-->B[障碍物]
    A-->C[道路]
    B-->D[道具块]
    B-->E[固定块]
    
    • 实现细节
    for (int i = 0; i < 15; ++i)
    {
    	std::vector<Vec2> map;
    	std::vector<int> prop;
    	for (int j = 0; j < 15; ++j)
    	{
    		map.push_back(road->getPositionAt(Vec2(i, j)) 
    		+ Vec2(deltaX + 16, deltaY - 480 + 16));
    
    		auto tileGidCol = collidable->getTileGIDAt(Point(i,j));
    		auto tileGidIte = _item->getTileGIDAt(Point(i, j));
    
    		if (tileGidCol > 0)
    			prop.push_back(1);//1代表该块为碰撞块
    
    		else if (tileGidIte > 0)
    			prop.push_back(4);//4代表该块为道具块
    
    		else
    			prop.push_back(0);//0代表无障碍物
    	}
    	_mapCoord.push_back(map);
    	_mapProp.push_back(prop);
    }
    • 说明

    地图的块属性被储存在

    std::vector<std::vector> _mapProp;


    每个块的位置被储存在

    std::vector<std::vector> _mapCoord;

    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 4 4 4 0 0 0 0 0 4 4 4 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 1 1 1 1 1 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 1 0 1 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 1 1 1 0 0 0 1 1 1 0 0 0
    0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    0 0 0 0 0 4 4 4 4 4 0 0 0 0 0
    0 0 0 0 0 0 4 4 4 0 0 0 0 0 0
    0 0 0 0 0 0 0 4 0 0 0 0 0 0 0
  • 玩家操作的人物:Player类

    • 图形化描述
    graph TB
    A[Player]-->B[移动]
    A-->C[动画]
    A-->D[人物属性]
    B-->E[描述移动方向的参数]
    D-->F[人物自身属性]
    D-->G[炸弹相关属性]
    
    
    • 人物实体

      image

      使用精灵帧中的图片实现

    • 人物移动

      以“瞬移”的形式移动
      移动的距离为速度的大小,单位是像素点
      移动的同时播放动画

    • 碰撞

      将player图片的四个角设置为碰撞点,则

      碰撞点检查 左上 左下 右上 右下
      向左移动 O O X X
      向右移动 X X O O
      向上移动 O X O X
      向下移动 X O X O

      由碰撞点的坐标计算出碰撞点所处的块
      再由块的属性决定是否发生了碰撞

    • 动画

      人物向A方向移动

      • 若向A方向移动的动画未播放,则播放此动画
      • 若向A方向移动的动画正在播放,则不做操作
      • 向A方向移动结束后,停止向A方向移动的动画
    • 人物属性

      graph TB
      A[人物自身属性]-->B[移动速度]
      A-->C[是否存活]
      
      graph TB
      A[炸弹相关属性]-->B[炸弹数量上限]
      A-->C[当前炸弹数量]
      A-->D[炸弹威力]
      
  • 优势的积累:Item类

    • 种类

      • 增加移动速度
      • 增加炸弹数量上限
      • 增加炸弹威力
    • 道具位置

      藏在道具块下
      数量一定,位置随机,由服务端产生的随机数种子确定

  • 杀人(唯一)利器:Bomb类

    • 性质

      • 属于障碍物
      • 安放两秒后爆炸
      • 爆炸产生的波持续0.6秒
      • Player碰到波后将会死亡
      • 有些小技巧
    • 碰撞

    bool MainScene::onContactBegin(const PhysicsContact& contact)
    {
    	auto objA = 
    	static_cast<Sprite*>(contact.getShapeA()->getBody()->getNode());
    	auto objB = 
    	static_cast<Sprite*>(contact.getShapeB()->getBody()->getNode());
    	auto nameA = objA->getName();
    	auto nameB = objB->getName();
    	
    	if (nameA == "player"&&nameB == "bomb")
    	{
    		log("player has been slayed");
    		auto player = static_cast<Player*>(objA);
    		player->_isAlive = false;
    	}
    
    	else if (nameA == "bomb"&&nameB == "player")
    	{
    		log("player has been slayed");
    		auto player = static_cast<Player*>(objB);
    		player->_isAlive = false;
    	}
    
    	return true;
    }

    借助物理碰撞的事件监听器,发生碰撞后,若是一方为波,一方为Player,则判定Player一方被消灭

  • 舞台:MainScene类

    • 加载项
      • 地图
      • Player
      • Item
    • 监听器
      • 键盘监听器
      • 碰撞监听器
    • 交互
      • 人物移动
      • 安放炸弹
      • 炸弹爆炸
      • 胜负判定
    • 网络通信
      • 服务端
      • 客户端(发射器)
      • 客户端(接收器)

网络通信

使用了boost库进行多线程(thread)运行和网络通信(asio)

  • 技术验证

    https://github.com/zwz1551719/boost-asio-demo

  • 房间选择界面的通信

    • 通信协议
      UDP

    • 数据处理
      同步

    • 规则

      • 创建房间的计算机作为服务器运行
      • 每个计算机都作为客户端运行
      • 服务器进行局域网广播

      room: 123, player: 0, map: 0, mapSeed: 32

      • 客户端接收广播,读取信息
      • 客户端请求连接

      connect

      • 服务器收到消息,回复客户端并分配Player

      successfully connected! player1

  • 游戏界面的通信

    • 通信协议
      UDP

    • 数据处理
      同步

    • 规则

      • 当本机人物位置发生变化或者安放炸弹时,客户端发出消息

      p2 u l b [123.45,123.45]

      • 服务端接收消息,并向其他连接的客户端转发
      • 客户端收到消息,解析消息,更新Player状态
      • 游戏中的逻辑判断发生在客户端本地而非服务端
  • 技术细节

class Room : public cocos2d::Layer
{
public:
    //...
    void static initBroadcast(Room* ptr);
    void static initClient(Room* ptr);
    void static initReceiver(Room* ptr);
    //...
}

类中的成员函数无法直接作为线程函数的参数,是因为成员函数隐含this指针,不符合调用函数的参数要求
所以需要将这些用于多线程的成员函数声明为static
而为了使这些函数能够处理类中的数据,必须将类的指针作为参数传入

_threadGroup.create_thread(std::bind(&initBroadcast, this));

房间创建

  • UI界面

    • 图形化描述
    graph TB
    
    A[Start]-->|选择地图|C[创建房间]
    A-->D[搜索房间]
    C-->E[房间命名]
    D-->F[房间列表]
    E-->G[进入房间]
    F-->|选择房间|G
    G-->H[进入游戏]
    
    • 选择地图

      • 提供两张比赛地图和一张测试地图
      • 不选择地图则默认使用测试地图
    • 创建房间

      • 无法创建房间名为空的房间
      • 房间名长度 <= 10 个英文字符
    • 房间列表

      • 创建或寻找并加入房间后跳转到相应房间界面
      • 显示房间的名称、人数、地图
  • 房间相关类

    • 图形化描述
    graph TB
    A[RoomVec]-->B[RoomItem_1]
    A-->C[RoomItem_2]
    B-->E[房间名]
    B-->F[玩家数量]
    B-->G[IP地址]
    C-->H[房间名]
    C-->I[玩家数量]
    C-->J[IP地址]
    
    • 单个房间的类:RoomItem

      • 属性
        • 房间名称
        • 玩家数量
        • IP地址
      • 方法
        • 获取房间名、玩家数量、IP地址
        • 修改玩家数量、IP地址(房间名不可更改)
    • 房间集合类:RoomVec

      • 属性
        • 封装std::vector
      • 方法
        • 向集合中添加新房间
        • 获取集合中房间数量
        • 通过名称查找集合中的房间
        • 通过IP地址查找集合中的房间
        • 获取指向房间首、末元素的迭代器
        • []运算符查找房间