小时候红白机上玩的的经典90坦克,看起来简单,做起来其实有点复杂,这里用原版素材还原了一个简版
预览
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190928215020698.gif)
工程结构
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190928220145136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTIyMzQxMTU=,size_16,color_FFFFFF,t_70)
游戏架构
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190928220744702.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTIyMzQxMTU=,size_16,color_FFFFFF,t_70)
包括场景:
步骤
菜单场景
对于图片,音乐,动画提前做缓存,提高后面使用效率
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("img/tank/tank.plist", "img/tank/tank.png");
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("img/item/item.plist", "img/item/item.png");
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("img/tank/blast.plist", "img/tank/blast/blast.png");
SimpleAudioEngine::getInstance()->preloadBackgroundMusic("sound/levelstarting.wav");
SimpleAudioEngine::getInstance()->preloadBackgroundMusic("sound/gamewin.wav");
SimpleAudioEngine::getInstance()->preloadBackgroundMusic("sound/gameover.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/bonus.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/brickhit.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/eexplosion.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/fexplosion.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/ice.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/life.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/moving.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/nmoving.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/shieldhit.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/shoot.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/steelhit.wav");
SimpleAudioEngine::getInstance()->preloadEffect("sound/tbonushit.wav");
Animation* player_born_animation = Animation::create();
player_born_animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("shield1.png"));
player_born_animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("shield2.png"));
player_born_animation->setDelayPerUnit(0.2);
AnimationCache::getInstance()->addAnimation(player_born_animation, "player_born_animation");
Animation* enemy_born_animation = Animation::create();
for (int i = 1; i <= 4; i++)
enemy_born_animation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("born" + std::to_string(i) + ".png"));
enemy_born_animation->setDelayPerUnit(0.2);
AnimationCache::getInstance()->addAnimation(enemy_born_animation, "enemy_born_animation");
游戏场景
场景中地图、坦克、子弹、道具都是子元素,每个元素都有对应的行为和状态
数据结构
地图
地图是由tmx文件导入的,提前在tiled编辑器里导出,每个方格是地图块的最小单位
方格类型包括:
不同方格属性不同,比如是否能让坦克通过,是否可以被子弹破坏
std::string map_name = "img/map/Round" + std::to_string(round) + ".tmx";
initWithTMXFile(map_name);
Size map_size = getContentSize();
Size map_array = getMapSize();
Size tile_size = Size(map_size.width / map_array.width, map_size.height / map_array.height);
由于地图加载场景中,尺寸大小会有缩放,所以需要精确计算每个方块的大小
玩家坦克
class Player : public Sprite
{
public:
virtual bool init();
CREATE_FUNC(Player);
public:
void initWithType(PlayerType player_type);
void setGameScene(GameScene* game_scene);
void move(float tm);
Bullet* shootSingle();
Vector<Bullet*> shootDouble();
void fetchItem(ItemType item_type);
void destroy();
public:
void setSize(Size size);
void setDirection(JoyDirection direction);
JoyDirection m_head_direction;
float m_bullet_interval;
bool m_moving;
PlayerStatus m_status;
PlayerWeapon m_weapon;
private:
GameScene* m_game_scene = nullptr;
Size m_size;
};
敌方坦克
enum EnemyType
{
NORMAL,
ARMOR,
SPEED
};
enum EnemyStatus
{
ESIMPLE,
ESHIELD
};
class Enemy : public Sprite
{
public:
virtual bool init();
CREATE_FUNC(Enemy);
void initWithType(EnemyType enemy_type);
void setSize(Size size);
public:
void setDirection(JoyDirection direction);
JoyDirection m_head_direction;
void move(float tm);
void changeDirection();
Bullet* shoot();
void hit();
void die();
int m_life;
EnemyType m_type;
bool m_moving;
EnemyStatus m_status;
private:
Size m_size;
float m_speed;
};
子弹
enum BulletType
{
BASE,
POWER
};
class Bullet : public cocos2d::Sprite
{
public:
virtual bool init();
void initWithDirection(JoyDirection direction, BulletType bullet_type = BASE);
CREATE_FUNC(Bullet);
public:
BulletType m_type;
bool m_hit_flag;
private:
void move(float tm);
JoyDirection m_direction;
};
道具
enum ItemType
{
ACTIVE,
STAR,
BOMB,
SHOVEL,
CLOCK,
MINITANK
};
class Item : public Sprite
{
public:
virtual bool init();
CREATE_FUNC(Item);
void initWithType(ItemType item_type);
public:
ItemType m_type;
};
- 不同类型(帽子、火力、炸弹、铲子、定时、命)
- 出现
- 被拾取
虚拟摇杆
专门实现了一个摇杆和射击的中间控制层,通过回调函数的方式施加于游戏场景,其中摇杆控制是稍微复杂一点的
基本思路:确定好摇杆中心的情况下,通过三角函数关系计算跟随触点与坐标横轴的角度,根据角度范围划分具体的方向,另外,在判定射击的时候要处理好左右屏和触摸先后的顺序
float Joypad::calcRad(Point p1, Point p2)
{
float xx = p2.x - p1.x;
float yy = p2.y - p1.y;
float xie = sqrt(pow(xx, 2) + pow(yy, 2));
float rad = yy >= 0 ? (acos(xx / xie)) : (PI * 2 - acos(xx / xie));
return rad;
}
Vec2 Joypad::getAnglePosition(float R, float rad)
{
return Point(R * cos(rad), R * sin(rad));
}
如图所示
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190928223241471.jpg)
void Joypad::onTouchesMoved(const std::vector<Touch*>& touches, Event* event)
{
if (!m_can_move)
return;
Point visible_origin = Director::getInstance()->getVisibleOrigin();
Size visible_size = Director::getInstance()->getVisibleSize();
Point point1 = touches.front()->getLocation();
Point point2 = touches.back()->getLocation();
Point wheel_center = m_wheel->getPosition();
float wheel_radius = m_wheel->getContentSize().width / 2;
Point left_point;
Point right_point;
if (point1.x < point2.x)
{
left_point = point1;
right_point = point2;
}
else
{
left_point = point2;
right_point = point1;
}
if (left_point.x < visible_origin.x + visible_size.width / 2)
{
Point point = left_point;
float distance = sqrt(pow(point.x - wheel_center.x, 2) + pow(point.y - wheel_center.y, 2));
float rad = calcRad(wheel_center, point);
if (distance >= wheel_radius)
{
m_stick->setPosition(wheel_center + getAnglePosition(wheel_radius, rad));
}
else
m_stick->setPosition(point);
float angle = rad * 180.0 / PI;
if (m_type == KEY4)
{
JoyDirection direction;
if (distance >= wheel_radius / 5)
{
if ((angle >= 0 && angle < 45) || (angle >= 315 && angle < 360))
direction = RIGHT;
else if (angle >= 45 && angle < 135)
direction = UP;
else if (angle >= 135 && angle < 225)
direction = LEFT;
else if (angle >= 225 && angle < 315)
direction = DOWN;
}
else
direction = NONE;
if (m_game_scene)
m_game_scene->onEnumDirection(direction);
}
else if (m_type == KEY8)
{
JoyDirection direction;
if (distance >= wheel_radius / 5)
{
if ((angle >= 0 && angle < 22.5) || (angle >= 337.5 && angle < 360))
direction = RIGHT;
else if (angle >= 22.5 && angle < 67.5)
direction = RIGHT_UP;
else if (angle >= 67.5 && angle < 112.5)
direction = UP;
else if (angle >= 112.5 && angle < 157.5)
direction = LEFT_UP;
else if (angle >= 157.5 && angle < 202.5)
direction = LEFT;
else if (angle >= 202.5 && angle < 247.5)
direction = LEFT_DOWN;
else if (angle >= 247.5 && angle < 292.5)
direction = DOWN;
else if (angle >= 292.5 && angle < 337.5)
direction = RIGHT_DOWN;
}
else
direction = NONE;
if (m_game_scene)
m_game_scene->onEnumDirection(direction);
}
else if (m_type == KEYANY)
{
if (m_game_scene)
m_game_scene->onAngleDirection(angle);
}
}
}
- 中间的stick要跟随触点,但是有界限
- 触点放开就自动归位
玩家控制
玩家控制包括方向键移动和开火射击
void GameScene::onEnumDirection(JoyDirection direction)
{
static JoyDirection pre_direction = NONE;
if (direction != pre_direction)
{
m_player1->setDirection(direction);
pre_direction = direction;
}
}
void GameScene::onFireBtn(bool is_pressed)
{
static bool pre_press_status = false;
if (is_pressed != pre_press_status)
{
if (is_pressed)
{
if (m_player_bullets.empty())
{
if (m_player1->m_weapon == SINGLE_GUN)
{
Bullet* bullet = m_player1->shootSingle();
addChild(bullet, kMapZorder);
m_player_bullets.pushBack(bullet);
}
else if (m_player1->m_weapon == DOUBLE_GUN)
{
Vector<Bullet*> double_bullets = m_player1->shootDouble();
for (Bullet* bullet : double_bullets)
{
addChild(bullet, kMapZorder);
m_player_bullets.pushBack(bullet);
}
}
}
schedule(schedule_selector(GameScene::emitPlayerBullet), m_player1->m_bullet_interval);
}
else
unschedule(schedule_selector(GameScene::emitPlayerBullet));
pre_press_status = is_pressed;
}
}
以上是游戏场景的几个回调函数,供摇杆类调用控制玩家
- 移动中如果遇到障碍就无法沿当前方向继续移动
- 移动遇到道具自动拾取并生效
- 可以在移动中开火
- 长按射击可以连续开火
- 每次只允许有限的子弹,下一发要等到前一发结束
敌方坦克行为
敌方坦克随机出现在地图上方出生点,随机切换方向,随机开火,同一批次生成的坦克有数量限制,总坦克数量有限制
void GameScene::generateEnemy(float tm)
{
if (m_enemies.size() == m_enemy_count)
{
unschedule(schedule_selector(GameScene::generateEnemy));
return;
}
if (m_enemies.size() >= kEnemyBatchTankCount)
return;
Size map_size = m_battle_field->getContentSize();
Size map_array = m_battle_field->getMapSize();
Size tile_size = Size(map_size.width / map_array.width, map_size.height / map_array.height);
float tank_type_factor = CCRANDOM_0_1();
EnemyType enemy_type;
if (tank_type_factor < 0.6)
enemy_type = NORMAL;
else if (tank_type_factor >= 0.6 && tank_type_factor < 0.9)
enemy_type = ARMOR;
else
enemy_type = SPEED;
Enemy* enemy = Enemy::create();
enemy->initWithType(enemy_type);
enemy->setSize(tile_size * 2 * kTankSizeFactor);
float tank_pos_factor = CCRANDOM_0_1();
if (tank_pos_factor <= 1.0 / 3)
enemy->setPosition(m_battle_field->getPositionX() + tile_size.width,
m_battle_field->getPositionY() + map_size.height - tile_size.height);
else if (tank_pos_factor >= 1.0 / 3 && tank_pos_factor < 2.0 / 3)
enemy->setPosition(m_battle_field->getPositionX() + map_size.width / 2,
m_battle_field->getPositionY() + map_size.height - tile_size.height);
else
enemy->setPosition(m_battle_field->getPositionX() + map_size.width - tile_size.width,
m_battle_field->getPositionY() + map_size.height - tile_size.height);
float tank_direction_factor = CCRANDOM_0_1();
if (tank_direction_factor < 0.25)
enemy->setDirection(UP);
else if (tank_direction_factor >= 0.25 && tank_direction_factor < 0.5)
enemy->setDirection(DOWN);
else if (tank_direction_factor >= 0.5 && tank_direction_factor < 0.75)
enemy->setDirection(LEFT);
else
enemy->setDirection(RIGHT);
if (m_is_clock)
enemy->m_moving = false;
addChild(enemy, kMapZorder);
m_enemies.pushBack(enemy);
}
- 固定时间间隔检查调度
- 移动中遇到障碍无法沿当前方向继续移动
- 可以在移动中开火
- 不同坦克类型移动速度不同
- 不同坦克射击子弹频率不同
子弹生成
子弹实际是由玩家或地方坦克射击产生的,但是需要在场景中调度
void GameScene::emitPlayerBullet(float tm)
{
if (m_player_bullets.empty())
{
if (m_player1->m_weapon == SINGLE_GUN)
{
Bullet* bullet = m_player1->shootSingle();
addChild(bullet, kMapZorder);
m_player_bullets.pushBack(bullet);
}
else if (m_player1->m_weapon == DOUBLE_GUN)
{
Vector<Bullet*> double_bullets = m_player1->shootDouble();
for (Bullet* bullet : double_bullets)
{
addChild(bullet, kMapZorder);
m_player_bullets.pushBack(bullet);
}
}
}
}
void GameScene::emitEnemyBullet(float tm)
{
for (Enemy* enemy : m_enemies)
{
float enemy_shoot_factor = CCRANDOM_0_1();
if (enemy->m_type == NORMAL && enemy_shoot_factor >= 0.5
|| enemy->m_type == ARMOR && enemy_shoot_factor >= 0.2
|| enemy->m_type == SPEED && enemy_shoot_factor >= 0.7)
{
Bullet* bullet = enemy->shoot();
addChild(bullet, kMapZorder);
m_enemy_bullets.pushBack(bullet);
}
}
}
- 固定时间间隔检查调度
- 射击后自动飞行
- 射出时出现在坦克头部
- 射出时的方向跟坦克方向相同
- 碰撞到不同障碍物有对应行为(普通子弹只能打土砖,火力子弹可以床钢板)
道具生成
道具是玩家射击到装甲车地方坦克时,根据概率随机选择类型出现在地图任意位置
void GameScene::generateItem()
{
SimpleAudioEngine::getInstance()->playEffect("sound/tbonushit.wav");
Size map_size = m_battle_field->getContentSize();
Size map_array = m_battle_field->getMapSize();
Size tile_size = Size(map_size.width / map_array.width, map_size.height / map_array.height);
ItemType item_type;
float item_type_factor = CCRANDOM_0_1();
if (item_type_factor < 1.0 / 6)
item_type = ACTIVE;
else if (item_type_factor >= 1.0 / 6 && item_type_factor < 2.0 / 6)
item_type = STAR;
else if (item_type_factor >= 2.0 / 6 && item_type_factor < 3.0 / 6)
item_type = BOMB;
else if (item_type_factor >= 3.0 / 6 && item_type_factor < 4.0 / 6)
item_type = SHOVEL;
else if (item_type_factor >= 4.0 / 6 && item_type_factor < 5.0 / 6)
item_type = CLOCK;
else
item_type = MINITANK;
Item* item = Item::create();
item->initWithType(item_type);
float item_posx_factor = CCRANDOM_0_1();
float pos_x = m_battle_field->getPositionX() + (map_size.width - tile_size.width * 2) * item_posx_factor;
float item_posy_factor = CCRANDOM_0_1();
float pos_y = m_battle_field->getPositionY() + (map_size.height - tile_size.height * 2) * item_posy_factor;
item->setPosition(pos_x, pos_y);
addChild(item, kItemZorder);
m_items.pushBack(item);
}
- 固定时间间隔检查调度
- 会有闪烁特效
- 不同类型被拾取后有对应效果
碰撞检查
有多种类型的碰撞检查
- 判断总部老鹰是否被击毁
- 判断敌方坦克是否被射击,死亡还是减血,掉落道具
- 判断玩家坦克是否被射击
- 判断玩家坦克移动是否遇到障碍
- 判断敌方坦克移动是否遇到障碍
- 判断玩家子弹对于场景的碰撞破坏
- 判断敌方子弹对于场景的碰撞破坏
具体逻辑都卸载update函数,保证每帧都会检测,及时反映
其中由于需要说明的是,对于地图中不同砖块被子弹碰撞或被坦克碰撞的逻辑,需要根据坐标定位到方块gid号,进而判断类型来做对应的处理,另外如果拾取到铲子道具,需要对地图中土砖变换为钢板,做方块类型的变更
bool isBulletCollide(Rect bounding_box, BulletType bullet_type);
bool isTankCollide(Rect bounding_box, JoyDirection direction);
bool isEagleHurt(Rect bounding_box);
void protectEagle();
void unprotectEagle();
元素管理
场景中的元素需要及时的回收和释放,避免内存浪费
- 出了地图边界的子弹销毁
- 碰撞的子弹销毁
- 被破坏的方块销毁
- 被击毁的地方坦克销毁
- 被击毁的玩家坦克销毁
游戏状态
如果地方坦克全部被击毁,则游戏胜利;如果玩家坦克命数用完或者总部老鹰被击毁则游戏结束;当前关卡通过后会进入下一关
void GameScene::gameWin()
{
CCLOG("game win");
SimpleAudioEngine::getInstance()->playBackgroundMusic("sound/gamewin.wav", false);
int next_round = m_round + 1;
if (next_round > kTotalRound)
next_round = 1;
Scene* scene = GameScene::createScene(next_round);
TransitionScene* transition_scene = TransitionFade::create(0.0, scene);
Director::getInstance()->replaceScene(transition_scene);
}
void GameScene::gameOver()
{
CCLOG("game over");
SimpleAudioEngine::getInstance()->playEffect("sound/gameover.wav", false);
m_is_over = true;
Point visible_origin = Director::getInstance()->getVisibleOrigin();
Size visible_size = Director::getInstance()->getVisibleSize();
Label *gameover_label = Label::createWithTTF("game over", "fonts/Marker Felt.ttf", 24);
gameover_label->setColor(Color3B::WHITE);
gameover_label->setPosition(visible_origin.x + visible_size.width / 2,
visible_origin.y - 30);
addChild(gameover_label, kLevelSplashZorder);
auto move_to = MoveTo::create(1.0, Point(visible_origin.x + visible_size.width / 2,
visible_origin.y + visible_size.height / 2));
gameover_label->runAction(move_to);
}
效果图
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190928215340119.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTIyMzQxMTU=,size_16,color_FFFFFF,t_70)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190928215353673.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTIyMzQxMTU=,size_16,color_FFFFFF,t_70)
后记
可以考虑扩展成双人联网合作模式
技术实现思路:
- 通过局域网连接
- 主玩家建立内嵌http websocket服务器接收从玩家的连接
- 互相之间通过websocket长连接通信
- 游戏逻辑在服务端计算,图形渲染在客户端各自渲染
代码
csdn:经典坦克
github:经典坦克
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)