Android游戏开发十日通(7)- 开发一个双人游戏

时间:2022-09-10 23:56:16

提要

       游戏需要分享才能获得快乐,想想你以前玩过的那些游戏,那些会是真正地存在你婶婶的脑海里?是独自一人躲在被窝里酣战PSP,还是和哥们在网吧一起开黑?是一个人单刷迅龙三连,还是和朋友联机怒刷黄黑龙?

       从来没有孤独的快乐,也从来没有孤独的游戏。

       今天要做的就是一个非常简单但有有点复杂的双人游戏-Air Hockey。

Android游戏开发十日通(7)- 开发一个双人游戏

通过这篇教程,你可以学到

1.cocos2d-x的生命周期;

2.如何让游戏兼容各种屏幕;

3.如何处理多点触控;

4.如何模拟斜碰...


创建游戏

还是老套路

终端进入 cocos2d-x-2.2/tools/project-creator/ ,执行
./create_project.py -project AirHockey -package com.maclab.airhockey -language cpp
在 /cocos2d-x-2.2/projects/AirHockey  中就有创建好的各平台的工程模板。

修改proj.android 下build_native.sh,添加一行指定NDK_ROOT

在eclipse中导入proj.android 工程,记得不要勾Copy to Project into workspace.

如果之前未将 /cocos2d-x-2.2/cocos2dx/platform/android/java 导入,在这里要导入。

创建软链接,终端进入 pro.android,执行命令:
ln -s ../Resources ./Resources
在Eclipse中刷新工程,Resources文件夹就出现了。

换一个狂拽酷炫点的图标

Android游戏开发十日通(7)- 开发一个双人游戏

将android工程中res文件夹下的icon.png换成这个就可以了。

在项目上右击,run as->android application
一切顺利的话工程就创建好了,如果没法运行,检查cocos2dx是否配置好。


cocos2d-x的生命周期

       这个问题上一篇教程直接略过了,这里来仔细分析下。

       AirHockey文件夹下有一个Classes文件夹和几个pro.*文件夹,Classes文件夹有两个类,一个AppDelegate,似私有继承自cocos2d::CCApplication,一个HelloWorldScence类,共有继承自cocos2d::CCLayer。

Android游戏开发十日通(7)- 开发一个双人游戏

大部分框架,基本上都可以分为两部分:
1. 一个入口主类,它定义了整个应用程序的生命周期,并提供一些全局的资源
2. 一些绘制到屏幕上的“页面”控件。


在cocos2d中,CCApplication主要做三件事情:
1. 控制应用程序的生命周期
2. 提供和管理一些全局的资源
3. 处理Touch
4. 循环绘制界面

应用程序的生命周期有一下几个虚方法:
applicationDidFinishLaunching();资源加载完成之后发生
applicationDidEnterBackground();程序进入后台被挂起
applicationWillEnterForeground();程序从后台被唤醒

下面是AppDelegate.cpp的内容

#include "AppDelegate.h"
#include "HelloWorldScene.h"

USING_NS_CC;

AppDelegate::AppDelegate() {

}

AppDelegate::~AppDelegate() 
{
}

bool AppDelegate::applicationDidFinishLaunching() {
    // 初始化导演类
    CCDirector* pDirector = CCDirector::sharedDirector();
    CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();

    pDirector->setOpenGLView(pEGLView);
	
    // 显示状态信息
    pDirector->setDisplayStats(true);

    // 设置FPS. 默认是60fps
    pDirector->setAnimationInterval(1.0 / 60);

    //创建一个场景. 它会自动释放内存
    CCScene *pScene = HelloWorld::scene();

    // 运行
    pDirector->runWithScene(pScene);

    return true;
}

//当程序被挂起的时候被调用,搓大招的时候老婆电话来了也会被调用
 This function will be called when the app is inactive. When comes a phone call,it's be invoked too
void AppDelegate::applicationDidEnterBackground() {
    CCDirector::sharedDirector()->stopAnimation();

    // 如果使用了SimpleAudioEngine,下面这行代码的注释请去掉
    // SimpleAudioEngine::sharedEngine()->pauseBackgroundMusic();
}

//当游戏从挂起状态回来的时候,方法被调用
void AppDelegate::applicationWillEnterForeground() {
    CCDirector::sharedDirector()->startAnimation();

    // 如果使用了SimpleAudioEngine,下面这行代码的注释请去掉
    // SimpleAudioEngine::sharedEngine()->resumeBackgroundMusic();
}

HelloWorldScence这个类就不多说了,所有游戏的逻辑,显示...都在这里调用,如果你还不熟悉,参考: Android游戏开发十日通(6)- 太空大战

下面是看linux和Android中怎样启动游戏的。


      首先看pro.linux下的代码,只有一个main.cpp

#include "../Classes/AppDelegate.h"
#include "cocos2d.h"
#include "CCEGLView.h"

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string>

USING_NS_CC;

int main(int argc, char **argv)
{
    // create the application instance
    AppDelegate app;
    CCEGLView* eglView = CCEGLView::sharedOpenGLView();
    eglView->setFrameSize(800, 480);
    return CCApplication::sharedApplication()->run();
}

CCEGLView封装了使用openGL作为显示底层API的一个基本的窗体的创建和控制,在linux中,窗口用的是GLFW库。

main中第一行生命了一个AppDelegate对象,第二行初始化一个CCEGLView指针,sharedOpenGLView中的内容如下:

CCEGLView* CCEGLView::sharedOpenGLView()  
{  
    
    if (s_pEglView == NULL)//s_pEglView是一个CCEGLView指针,静态成员变量  
    {  
        s_pEglView = new CCEGLView();  
        if(!s_pEglView->Create())//main中调用时,会执行这一步  
        {  
            delete s_pEglView;  
            s_pEglView = NULL;  
        }  
    }  
  
    return s_pEglView;  
}  

就是new一个对象。第三行设置窗口的大小,

我们再看看CCApplication::sharedApplication()->run()执行的又是什么东西。

int CCApplication::run()  
{  
    PVRFrameEnableControlWindow(false);  
  
    // Main message loop:  
    MSG msg;  
    LARGE_INTEGER nFreq;  
    LARGE_INTEGER nLast;  
    LARGE_INTEGER nNow;  
  
    QueryPerformanceFrequency(&nFreq);//获取当前系统频率和计数  
    QueryPerformanceCounter(&nLast);  
  
    // Initialize instance and cocos2d.  
    if (!applicationDidFinishLaunching())//虚函数,调用子类的重载,这里也会设置一些显示窗口的属性  
    {  
        return 0;  
    }  
  
    CCEGLView* pMainWnd = CCEGLView::sharedOpenGLView();获取CCEGLView的单一实例  
    pMainWnd->centerWindow();  
    ShowWindow(pMainWnd->getHWnd(), SW_SHOW);//这里显示窗口  
  
    while (1)//消息循环  
    {  
        if (! PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))  
        {  
            // 获取当前的计数  
            QueryPerformanceCounter(&nNow);  
  
            // 判断时间流逝,是否该绘制下一帧  
            if (nNow.QuadPart - nLast.QuadPart > m_nAnimationInterval.QuadPart)  
            {  
                nLast.QuadPart = nNow.QuadPart;  
                CCDirector::sharedDirector()->mainLoop();//渲染场景(清除显示设备,重绘场景)  
            }  
            else  
            {  
                Sleep(0);  
            }  
            continue;  
        }  
  
        if (WM_QUIT == msg.message)//获取退出消息,跳出循环  
        {  
            // Quit message loop.  
            break;  
        }  
  
        // 处理Windows消息  
        if (! m_hAccelTable || ! TranslateAccelerator(msg.hwnd, m_hAccelTable, &msg))  
        {  
            TranslateMessage(&msg);  
            DispatchMessage(&msg);  
        }  
    }  
  
    return (int) msg.wParam;  
}  

 总结一下,做了下面几件事:

(1)首先先获取当前系统的频率和计数。这是一个很大的值,所以用了一个LARGE_INTEGER型变量来存储。
(2)调用子类的applicationDidFinishLaunching(),执行进入程序后的一些初始化工作。
(3)获取CCEGLView单例,显示窗口。
(4)进入循环While(1),重绘每一帧的场景。

感兴趣的继续研究源码...

可以总结出linux下的启动流程:

Android游戏开发十日通(7)- 开发一个双人游戏


再看Android平台。

Android使用的是java,cocos2d-x使用的C++,这里其实是用C++开发android程序,需要的用到技术是JNI。(不知道JNI为何物的猛击我

看一下pro.android/jni/hellocpp/main.cpp

#include "AppDelegate.h"
#include "cocos2d.h"
#include "CCEventType.h"
#include "platform/android/jni/JniHelper.h"
#include <jni.h>
#include <android/log.h>

#define  LOG_TAG    "main"
#define  LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

using namespace cocos2d;

extern "C"
{
    
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JniHelper::setJavaVM(vm);

    return JNI_VERSION_1_4;
}

void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv*  env, jobject thiz, jint w, jint h)
{
    if (!CCDirector::sharedDirector()->getOpenGLView())
    {
        CCEGLView *view = CCEGLView::sharedOpenGLView();
        view->setFrameSize(w, h);

        AppDelegate *pAppDelegate = new AppDelegate();
        CCApplication::sharedApplication()->run();
    }
    else
    {
        ccGLInvalidateStateCache();
        CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
        ccDrawInit();
        CCTextureCache::reloadAllTextures();
        CCNotificationCenter::sharedNotificationCenter()->postNotification(EVENT_COME_TO_FOREGROUND, NULL);
        CCDirector::sharedDirector()->setGLDefaultValues(); 
    }
}

}

这里主要有两个函数:

(1)JNI_OnLoad,这个函数主要是用来告诉Android VM当前使用的是什么版本是Jni,如果不提供此函数,则默认使用Jni1.1版本。这个函数在加载交叉编译的库的时候,就会执行。
(2)Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit,这个函数很明显就是运行一个cocos2d-x的应用实例了,这和Win32是一样的,当然它多了一个openGlView的检测。一旦调用了它那么cocos2d-x游戏启动。
接下来再看看它们是在哪里被调用的。


看一下Android工程中的主类:

package com.maclab.airhockey;

import org.cocos2dx.lib.Cocos2dxActivity;
import org.cocos2dx.lib.Cocos2dxGLSurfaceView;

import android.os.Bundle;

public class AirHockey extends Cocos2dxActivity{
	
    protected void onCreate(Bundle savedInstanceState){
		super.onCreate(savedInstanceState);	
	}


    public Cocos2dxGLSurfaceView onCreateView() {
    	Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
    	// AirHockey should create stencil buffer
    	glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
    	
    	return glSurfaceView;
    }

    static {
        System.loadLibrary("cocos2dcpp");
    }     
}

cocos2dcpp是由C++交叉变异出来的库,在这里将它加载进来。这个activity是继承自Cocos3dxActivity,我们知道android应用的生命周期都是从onCreate开始的,在Cocos3dxActivity的onCreate方法中。调用了一个init函数,内容如下:

public void init() {
		
    	// FrameLayout
        ViewGroup.LayoutParams framelayout_params =
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                                       ViewGroup.LayoutParams.FILL_PARENT);
        FrameLayout framelayout = new FrameLayout(this);
        framelayout.setLayoutParams(framelayout_params);

        // Cocos2dxEditText layout
        ViewGroup.LayoutParams edittext_layout_params =
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
                                       ViewGroup.LayoutParams.WRAP_CONTENT);
        Cocos2dxEditText edittext = new Cocos2dxEditText(this);
        edittext.setLayoutParams(edittext_layout_params);

        // ...add to FrameLayout
        framelayout.addView(edittext);

        // Cocos2dxGLSurfaceView
        this.mGLSurfaceView = this.onCreateView();

        // ...add to FrameLayout
        framelayout.addView(this.mGLSurfaceView);

        // Switch to supported OpenGL (ARGB888) mode on emulator
        if (isAndroidEmulator())
           this.mGLSurfaceView.setEGLConfigChooser(8 , 8, 8, 8, 16, 0);

        this.mGLSurfaceView.setCocos2dxRenderer(new Cocos2dxRenderer());
        this.mGLSurfaceView.setCocos2dxEditText(edittext);

        // Set framelayout as the content view
		setContentView(framelayout);
	}
	

在这里Cocos2dxActivity做的就是创建Cocos2dxGLSurfaceView,并设置了Cocos2dxRenderer和Cocos2dxEditText,然后添加到FramLayout。

而NativeIint方法在则在Cocos2dxRenderer中被调用:

	@Override
	public void onSurfaceCreated(final GL10 pGL10, final EGLConfig pEGLConfig) {
		Cocos2dxRenderer.nativeInit(this.mScreenWidth, this.mScreenHeight);
		this.mLastTickInNanoSeconds = System.nanoTime();
	}


最后总结层次调用关系如下:

Android游戏开发十日通(7)- 开发一个双人游戏


是不是有点头晕~_~,没关系,别忘了我们最初的目标 - 做狂拽炫酷的游戏!


为游戏支持不同分辨率的屏幕

       要让游戏在不同分辨率下都获得良好的用户体验,应该满足这几个要求:

背景图填满整个画面,不出现黑边;
背景图的主要内容都显示在屏幕上,尽可能少的裁剪图片(减少超出屏幕可视区域的部分);
如果背景图需要放大,尽可能减小放大的比例,避免放大后出现明显的模糊;
用户界面的文字标签、按钮在任何分辨率下都应该完整显示,并且容易交互。


       相信很多游戏开发者开发者选择ios作为首选开发平台的一个主要原因就是IOS没有坑爹的碎片化,系统没有碎片化,屏幕没有碎片化, 机器性能没有碎片化....但到了Android,一切就成了噩梦。

      一个解决屏幕碎片化的方法就是为不同的屏幕分辨率准备不同的资源文件。另一种方法是按比例缩放。

缩放比例的做法比较方便,在AppDelegate::applicationDidFinishLaunching()中添加代码:

CCEGLView::sharedOpenGLView()->setDesignResolutionSize(800, 480,kResolutionExactFit);

在这里,设定 超过这个或者小于这个分辨率,cocos2d-x会做自适应处理。
第三个参数是自适应分辨率的规则,有3种
kResolutionExactFit 在指定的应用的现实区域会尝试去保持原始比例,但是有可能会发生变形,这时候你的应用将会被拉伸或者压缩
kResolutionNoBorder 在指定的应用文件区域内不会出现变形,但是可以能有一些裁剪
kResolutionShowAll 在指定的应用文件区域内不会出现变形,将保持原样。但是两边会出现黑边。

在800*480窗口下,正常现实的效果如下:

Android游戏开发十日通(7)- 开发一个双人游戏



CCEGLView::sharedOpenGLView()->setDesignResolutionSize(200, 480,kResolutionExactFit);

Android游戏开发十日通(7)- 开发一个双人游戏


CCEGLView::sharedOpenGLView()->setDesignResolutionSize(200, 480,kResolutionShowAll);

Android游戏开发十日通(7)- 开发一个双人游戏

    CCEGLView::sharedOpenGLView()->setDesignResolutionSize(200, 480,kResolutionNoBorder);

Android游戏开发十日通(7)- 开发一个双人游戏


如果是采用准备多套资源文件的方法,则在Resources文件夹中创建img/xhdip和img/hdip文件夹。

将下载好的图片放到对应的文件夹中,

还是修改AppDelegate.cpp的代码。

首先是添加一个结构体,在定义几个static变量:

typedef struct tagResource
{
    cocos2d::CCSize size;
    char directory[100];
}Resource;

static Resource smallResource  =  { cocos2d::CCSizeMake(1024, 600),   "img/hdip" };
static Resource mediumResource =  { cocos2d::CCSizeMake(1280, 720),  "img/xhdip" };
static cocos2d::CCSize designResolutionSize = cocos2d::CCSizeMake(1024, 600);



我这里只定义了small和medium,默认的屏幕是1024*600,因为这是我的平板的屏幕。
然后修改AppDelegate::applicationDidFinishLaunching,在应用启动之后马上初始化资源。


bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    CCDirector* pDirector = CCDirector::sharedDirector();
    CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();
    pDirector->setOpenGLView(pEGLView);

//CCEGLView::sharedOpenGLView()->setDesignResolutionSize(200, 480,kResolutionShowAll);
    CCEGLView::sharedOpenGLView()->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, kResolutionNoBorder);
    
     CCSize frameSize = pEGLView->getFrameSize();
     
     std::vector<std::string> resDirOrders;
      // if the frame's height is larger than the height of medium resource size, select large resource.
    if (frameSize.height > mediumResource.size.height)
    { 
        pDirector->setContentScaleFactor(mediumResource.size.height/designResolutionSize.height);
        resDirOrders.push_back(mediumResource.directory);
    }
    
    // if the frame's height is larger than the height of small resource size, select medium resource.
    else
    { 
        pDirector->setContentScaleFactor(smallResource.size.height/designResolutionSize.height);
        resDirOrders.push_back(smallResource.directory);
    }
    resDirOrders.push_back("sounds");  
    resDirOrders.push_back("/");
    CCFileUtils::sharedFileUtils()->setSearchResolutionsOrder(resDirOrders);   
  // turn on display FPS    pDirector->setDisplayStats(true);   
   // set FPS. the default value is 1.0/60 if you don't call this 
   pDirector->setAnimationInterval(1.0 / 60);    
  // create a scene. it's an autorelease object     
  CCScene *pScene = HelloWorld::scene();     
  // run    
  pDirector->runWithScene(pScene);  
  return true;  
}



这样,资源的搜索路径就被设置好了 -大屏幕设备加载资源时,首先查找Resources/img/xhdip,然后搜Resources;小屏幕设备加载资源时,首先查找Resources/img/hdip,然后搜Resources。


预加载音效

将资源中的*.wav文件都放到工程的Resources文件夹中。

首先修改pro.linux 下的MakeFile,

INCLUDES = -I.. -I../Classes\
                       -I$(COCOS_ROOT)/CocosDenshion/include  
SHAREDLIBS += -lcocos2d -lcocosdenshion  

在AppDelegate.cpp中添加头文件和命名空间,

#include "SimpleAudioEngine.h"
using namespace CocosDenshion ;

在ApplicationDidFinishLanuching中添加加载的语句,

    //Setting sounds
    std::string fullPath = CCFileUtils::sharedFileUtils()->fullPathForFilename("hit.wav");
    SimpleAudioEngine::sharedEngine()->preloadEffect(fullPath.c_str());
    fullPath = CCFileUtils::sharedFileUtils()->fullPathForFilename("score.wav");
    SimpleAudioEngine::sharedEngine()->preloadEffect(fullPath.c_str());


创建自己的CCSprite

首先看一下何为Sprite.

        Spirite是渲染框架中最小的一个单元了,游戏精灵是游戏制作中非常重要的一个概念。它是游戏内容最好的呈现。游戏开发者主要的控制对象也会是精灵。精灵对象可以是游戏画面中的任何一对象,可以是游戏中的主角,也可以是汽车,或者是一个树,一个高富帅,一个穷屌丝,哪怕是一片树叶也可以是一个精灵对象。从技术角度来看,精灵对象就是一个可以不断变化的图片,而其本身也具备了一些特殊的属性和方法,比如纹理、尺寸、翻转、透明以及角度的改变。精灵作为游戏的重要元素,最后也会被开发者新赋予一些新的逻辑或者物理属性。

下面是我们继承CCSprite的GameSprite类。

gamesprite.h

#ifndef GAMESPRITE_H
#define GAMESPRITE_H
#include "cocos2d.h"
using namespace cocos2d;

class GameSprite : public CCSprite
{
public:

    CC_SYNTHESIZE(CCPoint, _nextPosition, NextPosition);
    CC_SYNTHESIZE(CCPoint, _vector, Vector);
    CC_SYNTHESIZE(CCTouch *, _touch, Touch);

    GameSprite(void);
    ~GameSprite(void);

    static GameSprite* gameSpriteWithFile(const char *pszFileName);
    virtual void setPosition(const CCPoint& pos);
    float radius(){return getTexture()->getContentSize().width * 0.5f;}
};

#endif // GAMESPRITE_H


gamesprite.cpp

#include "gamesprite.h"

GameSprite::GameSprite(void)
{
    _vector = ccp(0,0);
}

GameSprite::~GameSprite(void)
{
}

GameSprite* GameSprite::gameSpriteWithFile(const char *pszFileName) {
    GameSprite * sprite = new GameSprite();
    if (sprite && sprite->initWithFile(pszFileName)) {
        sprite->autorelease();
        return sprite;
    }
    CC_SAFE_DELETE(sprite);
    return NULL;
}

void GameSprite::setPosition(const CCPoint& pos) {
    CCSprite::setPosition(pos);
    if (!_nextPosition.equals(pos)) {
        _nextPosition = pos;
    }
}


gameSpriteWithFIle用于从文件创建一个自动释放内存的Sprite;setPosition用于改变Sprite位置,同时更新_nextPosition; radius这个inline函数用于返回物体的半径,也就是sprite宽度的一半。

修改proj.linux/MakeFile,将新添加的 cpp 索引进来。

SOURCES = main.cpp \  
        ../Classes/AppDelegate.cpp \  
        ../Classes/HelloWorldScene.cpp\  
        ../Classes/gamesprite.cpp  




android版本的编译需要修改proj.android/jni/Android.mk

LOCAL_SRC_FILES := hellocpp/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../Classes/HelloWorldScene.cpp\  
                  ../../Classes/gamesprite.cpp


搭建场景

场景的搭建都在HelloWorld::init()中处理。

// on "init" you need to initialize your instance
bool HelloWorld::init()
{

    _player1Score = 0;
    _player2Score = 0;
    _screenSize = CCDirector::sharedDirector()->getWinSize();

    //////////////////////////////
    // 1. super init first
    if ( !CCLayer::init() )
    {
        return false;
    }
    
    CCSize visibleSize = CCDirector::sharedDirector()->getVisibleSize();
    CCPoint origin = CCDirector::sharedDirector()->getVisibleOrigin();

    // add court background
    CCSprite* court = CCSprite::create("court.png");

    // position court on the center of the screen
    court->setPosition(ccp(visibleSize.width* 0.5 + origin.x, visibleSize.height* 0.5 + origin.y));

    // add court as a child to this layer
    this->addChild(court, 0);

    _player1 = GameSprite::gameSpriteWithFile("mallet.png");
    _player1->setPosition(ccp(_player1->radius() * 2,_screenSize.height * 0.5) );
    this->addChild(_player1,1);
    _player2 = GameSprite::gameSpriteWithFile("mallet.png");
    _player2->setPosition(ccp(_screenSize.width - _player1->radius() * 2, _screenSize.height * 0.5));
    this->addChild(_player2,1);
    _ball = GameSprite::gameSpriteWithFile("puck.png");
    _ball->setPosition(ccp( _screenSize.width* 0.5 - 2 * _ball->radius(), _screenSize.height * 0.5));
    this->addChild(_ball,1);

    _players = CCArray::create(_player1, _player2, NULL);
    _players->retain();

    _player1ScoreLabel = CCLabelTTF::create("0", "Arial", 60);
    _player1ScoreLabel->setPosition(ccp( _screenSize.width * 0.5 - 80, _screenSize.height - 60));
    this->addChild(_player1ScoreLabel, 2);

    _player2ScoreLabel = CCLabelTTF::create("0", "Arial", 60);
    _player2ScoreLabel->setPosition(ccp(_screenSize.width * 0.5 + 80, _screenSize.height - 60));
    this->addChild(_player2ScoreLabel, 2);

    //listen for touches
    this->setTouchEnabled(true);
    //create main loop
    this->schedule(schedule_selector(HelloWorld::update));
    return true;
}

非常简单,无非是初始化一些变量,在场景中添加背景,添加Sprite,记分牌。激活触控,设置更新的回调函数。

运行之后的效果:

Android:、

Android游戏开发十日通(7)- 开发一个双人游戏

Linux:

Android游戏开发十日通(7)- 开发一个双人游戏


在析构函数中释放之前开辟的内存:

HelloWorld::~HelloWorld()
{
    CC_SAFE_RELEASE(_players);
}


动起来

一步步来,首先是处理多点触控。

在HelloWorld.h中添加三个关于主控的函数:

    virtual void ccTouchesBegan(CCSet* pTouches, CCEvent*event);
    virtual void ccTouchesMoved(CCSet* pTouches, CCEvent*event);
    virtual void ccTouchesEnded(CCSet* pTouches, CCEvent*event);

网文生意,这三个函数分别在触控开始,触控移动,触控结束时调用。

这里主要要处理的问题就是手指和GamePlayer对应问题,用下面的代码就可以解决。

    CCSetIterator i;
    CCTouch* touch;
    CCPoint tap;
    GameSprite * player;
    for( i = pTouches->begin(); i != pTouches->end(); i++)
    {
        touch = (CCTouch*) (*i);
        if(touch)
        {
            tap = touch->getLocation();
            for (int p = 0; p < 2; p++)
            {
                player = (GameSprite *) _players->objectAtIndex(p);
               //Do some thing.
            }
        }
    }

前四行声名了4个局部变量,i是CCSet的迭代器,touch是指向CCTouch的指针,CCPoint用于记录触碰的位置,player是一个GameSpirit的指针。

第五行遍历所有的触控点,接下来对每个触控点进行处理。第六行将迭代器转换为CCTouch指针,接下来对于当前的触控点,再遍历所有_players中的player,然后进行处理。

三个关于触控的函数的处理流程相同,不同的是做的工作不一样。

ccTouchesBegan中要做的是将触控点与对应的player连接起来,ccTouchesMoved要做的是更新player的位置,ccTouchesEnded要做的是清空触控信息。具体的实现如下:

void HelloWorld::ccTouchesBegan(CCSet *pTouches, CCEvent *event)
{
    CCSetIterator i;
    CCTouch* touch;
    CCPoint tap;
    GameSprite * player;
    for( i = pTouches->begin(); i != pTouches->end(); i++)
    {
        touch = (CCTouch*) (*i);
        if(touch)
        {
            tap = touch->getLocation();
            for (int p = 0; p < 2; p++)
            {
                player = (GameSprite *) _players->objectAtIndex(p);
                if (player->boundingBox().containsPoint(tap))
                {
                    player->setTouch(touch);
                }
            }
        }
    }
}

void HelloWorld::ccTouchesMoved(CCSet *pTouches, CCEvent *event)
{
    CCSetIterator i;
    CCTouch* touch;
    CCPoint tap;
    GameSprite * player;
    for( i = pTouches->begin(); i != pTouches->end(); i++)
    {
        touch = (CCTouch*) (*i);
        if(touch)
        {
            tap = touch->getLocation();
            for (int p = 0; p < 2; p++)
            {
                player = (GameSprite *) _players->objectAtIndex(p);
                if (player->getTouch() != NULL && player->getTouch() == touch)
                {
                    CCPoint nextPosition = tap;
                    //keep player inside screen
                    if (nextPosition.x < player->radius())
                        nextPosition.x = player->radius();
                    if (nextPosition.x > _screenSize.width - player->radius())
                        nextPosition.x = _screenSize.width - player->radius();
                    if (nextPosition.y < player->radius())
                        nextPosition.y = player->radius();
                    if (nextPosition.y > _screenSize.height - player->radius())
                        nextPosition.y = _screenSize.height - player->radius();
                    //keep player inside its court
                    if (player->getPositionX() < _screenSize.width *0.5f)
                    {
                        if (nextPosition.x > _screenSize.width * 0.5 - player->radius())
                        {
                            nextPosition.x = _screenSize.width * 0.5 - player->radius();
                        }
                    } else{
                        if (nextPosition.x < _screenSize.width * 0.5 + player->radius())
                        {
                            nextPosition.x = _screenSize.width * 0.5 + player->radius();
                        }
                    }
                    player->setNextPosition(nextPosition);
                    player->setVector(ccp(tap.x - player->getPositionX(), tap.y - player->getPositionY()));
                }
            }
        }
    }
}

void HelloWorld::ccTouchesEnded(CCSet *pTouches, CCEvent *event)
{
    CCSetIterator i;
    CCTouch* touch;
    CCPoint tap;
    GameSprite * player;
    for( i = pTouches->begin(); i != pTouches->end(); i++)
    {
        touch = (CCTouch*) (*i);
        if(touch)
        {
            tap = touch->getLocation();
            for (int p = 0; p < 2; p++)
            {
                player = (GameSprite *) _players->objectAtIndex(p);
                if (player->getTouch() != NULL && player->getTouch() == touch) {
                    player->setTouch(NULL);
                    player->setVector(ccp(0,0));
                }
            }
        }
    }

}

编译,运行,我点~为什么不能动!这是为~什么!

因为根本就没有在游戏中更新player的位置啊。在HelloWorld::update中添加下面的代码:

    //move pieces to next position
    _player1->setPosition(_player1->getNextPosition());
    _player2->setPosition(_player2->getNextPosition());
    _ball->setPosition(_ball->getNextPosition());

运行效果:

Android游戏开发十日通(7)- 开发一个双人游戏


碰撞检测

这里的碰撞检测要处理两个方面,一个是player和小球的碰撞,一个是墙壁和小球的碰撞。player和小球的碰撞会改变小球的运动路线,墙壁和小球碰撞,要么反弹,要么得分。

player和小球碰撞要注意一个“陷入”的问题,就是在检测的时候要预测一下小球的走位,不然小球和player就会发生重叠。

Android游戏开发十日通(7)- 开发一个双人游戏

还有就是小球和player发生碰撞的情况属于斜碰,在计算的时候,首先通过player和小球的速度计算出一个附加在小球上的力,然后再计算出一个角度,最后设置小球的速度。

和墙壁的碰撞就简单一些了,也要注意陷入的问题。然后就是得分的话要更新记分牌。

最后别忘了在碰撞的时候添加音效。完整的update函数如下:

void HelloWorld::update(float dt)
{
    CCPoint ballNextPosition = _ball->getNextPosition();
    CCPoint ballVector = _ball->getVector();
    ballVector = ccpMult(ballVector, 0.98f);
    ballNextPosition.x += ballVector.x;
    ballNextPosition.y += ballVector.y;

    //test for puck and mallet collision
    float squared_radii = pow(_player1->radius() + _ball->radius(), 2);

    GameSprite * player;
    CCPoint playerNextPosition;
    CCPoint playerVector;
    for (int p = 0; p < 2; p++) {

        player = (GameSprite *) _players->objectAtIndex(p);
        playerNextPosition = player->getNextPosition();
        playerVector = player->getVector();

        float diffx = ballNextPosition.x - player->getPositionX();
        float diffy = ballNextPosition.y - player->getPositionY();

        float distance1 = pow(diffx, 2) + pow(diffy, 2);
        float distance2 = pow(_ball->getPositionX() - playerNextPosition.x, 2) + pow(_ball->getPositionY() - playerNextPosition.y, 2);

        if (distance1 <= squared_radii || distance2 <= squared_radii) {

            float mag_ball = pow(ballVector.x, 2) + pow(ballVector.y, 2);
            float mag_player = pow (playerVector.x, 2) + pow (playerVector.y, 2);

            float force = sqrt(mag_ball + mag_player);
            float angle = atan2(diffy, diffx);

            ballVector.x = force * cos(angle);
            ballVector.y = force * sin(angle);

            ballNextPosition.x = playerNextPosition.x + (player->radius() + _ball->radius() + force) * cos(angle);
            ballNextPosition.y = playerNextPosition.y + (player->radius() + _ball->radius() + force) * sin(angle);

            SimpleAudioEngine::sharedEngine()->playEffect("score.wav");
        }
    }

    //check collision of ball and sides
    if (ballNextPosition.x < _ball->radius()) {
        ballNextPosition.x = _ball->radius();
        ballVector.x *= -0.8f;
        SimpleAudioEngine::sharedEngine()->playEffect("hit.wav");
    }

    if (ballNextPosition.x > _screenSize.width - _ball->radius()) {
        ballNextPosition.x = _screenSize.width - _ball->radius();
        ballVector.x *= -0.8f;
        SimpleAudioEngine::sharedEngine()->playEffect("hit.wav");
    }
    //ball and top of the court
    if (ballNextPosition.y > _screenSize.height - _ball->radius()) {
        if (_ball->getPosition().x < _screenSize.width * 0.5f - 40 * 0.5f ||
            _ball->getPosition().x > _screenSize.width * 0.5f + 40 * 0.5f) {
            ballNextPosition.y = _screenSize.height - _ball->radius();
            ballVector.y *= -0.8f;
            SimpleAudioEngine::sharedEngine()->playEffect("hit.wav");
        }
    }
    //ball and bottom of the court
    if (ballNextPosition.y < _ball->radius() ) {
        if (_ball->getPosition().x < _screenSize.width * 0.5f - 40 * 0.5f ||
            _ball->getPosition().x > _screenSize.width * 0.5f + 40 * 0.5f) {
            ballNextPosition.y = _ball->radius();
            ballVector.y *= -0.8f;
            SimpleAudioEngine::sharedEngine()->playEffect("hit.wav");
        }
    }

    //finally, after all checks, update ball's vector and next position
    _ball->setVector(ballVector);
    _ball->setNextPosition(ballNextPosition);


    //check for goals!
    if (ballNextPosition.x  < 50) {
        this->playerScore(2);

    }
    if (ballNextPosition.x > _screenSize.width-50) {
        this->playerScore(1);
    }

    //move pieces to next position
    _player1->setPosition(_player1->getNextPosition());
    _player2->setPosition(_player2->getNextPosition());
    _ball->setPosition(_ball->getNextPosition());
}

最后还要实现一个得分处理的函数,主要是更新记分牌,归位,换发球。

void HelloWorld::playerScore (int player) {

    SimpleAudioEngine::sharedEngine()->playEffect("score.wav");

    _ball->setVector(CCPointZero);

    char score_buffer[10];

    //if player 1 scored...
    if (player == 1) {

        _player1Score++;
        sprintf(score_buffer,"%i", _player1Score);
        _player1ScoreLabel->setString(score_buffer);
        //move ball to player 2 court
        _ball->setNextPosition(ccp( _screenSize.width* 0.5 + 2 * _ball->radius(), _screenSize.height * 0.5));

    //if player 2 scored...
    } else {

        _player2Score++;
        sprintf(score_buffer,"%i", _player2Score);
        _player2ScoreLabel->setString(score_buffer);
        //move ball to player 1 court
        _ball->setNextPosition(ccp( _screenSize.width* 0.5 - 2 * _ball->radius(), _screenSize.height * 0.5));
    }
    //move players to original position
    _player1->setPosition(ccp(_player1->radius() * 2,_screenSize.height * 0.5));
    _player2->setPosition(ccp(_screenSize.width - _player1->radius() * 2, _screenSize.height * 0.5));

    //clear current touches
    _player1->setTouch(NULL);
    _player2->setTouch(NULL);
}

最终效果

Android游戏开发十日通(7)- 开发一个双人游戏



参考

【玩转cocos2d-x之三】cocos2d-x游戏是怎么跑起来的 -  http://blog.csdn.net/jackystudio/article/details/12554167

【玩转cocos2d-x之四】cocos2d-x怎么实现跨平台 - http://blog.csdn.net/jackystudio/article/details/12610287

Cocos2d-x 2.0 自适应多种分辨率 - http://dualface.github.io/blog/2012/08/17/cocos2d-x-2-dot-0-multi-resolution/

Cocos2d-x by Example Beginner's Guide