移动平台下的游戏有着区别于其他平台独特的优势,比如携带便捷,基于地理位置的社交,触摸的用户体验等等。
公司正在开发的游戏也利用了地理位置的优势,开发了社交功能,即将附近的玩家显示出来,可以和他们对战,聊天等等,之前的做法是利用cocos2d-x模拟的地图,根据服务器下发的附近玩家的信息显示,光是手势就实现了两种(捏合和滑动这两种在地图上使用最多的手势),但是效果很不好,于是决定使用ios自带的地图框架,与cocos2d-x结合来实现!于是,一个简单的CCMapView诞生了!
1.结构
cocos2d-x 主要特点是跨平台,于是需要定义一个跨平台的接口,然后在不同平台上做不同实现(因为地图UI是基于系统的),今天主要以ios平台为例
回调函数指针: CCMapViewCallback
跨平台接口类: CCMapView
ios平台实现: CCMapViewIOS
ios平台对MKMapView的封装类: MapView
2.接口:
(1)回调函数:CCMapViewCallback
由于objective c和c++通信只能通过指针,所以我们需要定义通信的函数指针类型,于是我定义了两个接口,一个用于系统定位完成之后回调游戏,另一个用于选中地图中的annotation之后回调游戏(这种从ios到游戏的接口可能是比较复杂一点,相当于回调。至于从游戏调用ios则感觉顺畅的多,只要保存一个对应类型的指针即可)
#ifndef Cocos2dxMap_MapViewCallback_h #define Cocos2dxMap_MapViewCallback_h typedef void (*DidLocatedCallback)(double latitude, double longtitude); typedef void (*SelectedPlayerCallback)(unsigned long long playerID); #endif
(2)跨平台接口类:CCMapView
首先定义了一个附近玩家信息的结构体,这个结构体是我们在地图中显示annotation时需要的信息
typedef struct { unsigned long long mPlayerID; bool mIsMan; std::string mPlayerName; float mLatitude; float mLongtitude; } CCPlayerInMap;
然后定义了一个简单的接口类,用于封装系统的地图空间,使用了组合的方式,这里利用void*的特点,实现了跨平台
class CCMapView: public cocos2d::CCNode { public: CCMapView(cocos2d::CCRect const& frame); ~CCMapView(); public: void StartLocating(); void StopLocating(); // in meters void SetRadius(float radius); void AddPlayer(CCPlayerInMap const& ccPlayerInMap); void RemoveAllPlayers(); public: void SetPosition(cocos2d::CCPoint const& pos); public: void SetDidLocatedCallback(DidLocatedCallback callback); void SetSelectedPlayerCallback(SelectedPlayerCallback callback); private: void* mMapView; };
3.实现
定义一个类MapView来继承MKMapView并且实现CLLocationManager和MKMapViewDelegate协议(原谅我比较懒并且对objective-c的语法没啥自信吧,分开实现或许是更好的选择。。。),继承MKMapView是为了使用ios原生地图控件,实现CLLocationManager和MKMapViewDelegate协议是为了提高定位和地图显示和交互的回调接口!
@interface MapView : MKMapView <CLLocationManagerDelegate, MKMapViewDelegate> { // Location CLLocationManager* mLocationManager; float mRadius; // for refreshing NSMutableArray* mAnnoArray; // Data NSMutableArray* mNearbyPlayers; DidLocatedCallback mDidLocatedCallback; SelectedPlayerCallback mSelectedPlayerCallback; UIImage* mMaleImage; UIImage* mMaleSelectedImage; UIImage* mFemaleImage; UIImage* mFemaleSelectedImage; MapAnnotationView* mPreMapAnnotionView; } @property (retain) CLLocationManager* mLocationManager; @property (readwrite) float mRadius; @property (nonatomic, retain) NSMutableArray* mAnnoArray; @property (nonatomic, retain) NSMutableArray* mAnnoViewArr; @property (nonatomic, retain) NSMutableArray* mNearbyPlayers; @property (nonatomic, readwrite) DidLocatedCallback mDidLocatedCallback; @property (nonatomic, readwrite) SelectedPlayerCallback mSelectedPlayerCallback; - (void)StartLocating; - (void)StopLocating; - (void)AddNearbyPlayer:(PlayerInMap *) player; - (void)RemoveAllPlayers; - (void)HandleTap:(UIGestureRecognizer *)recognizer; @end
在实现中,我运用了一个小技巧,绕过了大头针annotation不设置title就不能接受回调的显示,简单来说就是为每个MapAnnotationView添加一个Tap手势注册,而不是通过其自身的点击事件,其它的写法和一般的地图写法并没有太大差别,如果你的项目中没有这么强的定制性,可以添加一个title,就可以在didSelectedxxx函数中接收到点击回调了!
(ps:重点是在定位完成后和点击地图中的annotation调用设置好的函数指针回调游戏)
// // MapView.m // Cocos2dxMap // // Created by quyou on 13-2-22. // // #import "MapView.h" #import "MapAnnotation.h" #import "CCMapViewCallback.h" @implementation MapView @synthesize mLocationManager; @synthesize mRadius; @synthesize mAnnoArray; @synthesize mAnnoViewArr; @synthesize mNearbyPlayers; @synthesize mDidLocatedCallback; @synthesize mSelectedPlayerCallback; - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { // Initialization code self.mLocationManager = [[[CLLocationManager alloc] init] autorelease]; if (![CLLocationManager locationServicesEnabled]) { NSLog(@"User has opted out of location services!"); return self; } mMaleImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"male" ofType:@"png"]]; mMaleSelectedImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"male_selected" ofType:@"png"]]; mFemaleImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"female" ofType:@"png"]]; mFemaleSelectedImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"female_selected" ofType:@"png"]]; mPreMapAnnotionView = nil; mAnnoArray = [[NSMutableArray alloc] init]; self.mLocationManager.delegate = self; self.mLocationManager.desiredAccuracy = kCLLocationAccuracyBest; // in meters self.mLocationManager.distanceFilter = 5.0f; // init properties self.mRadius = 1000.0f; // Mapview delegate self.delegate = self; } return self; } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { NSLog(@"Location manager error: %@", [error description]); } - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { CLLocation* newLocation = [locations lastObject]; if (newLocation != NULL) { NSDate* time = newLocation.timestamp; NSTimeInterval timePeriod = [time timeIntervalSinceNow]; if(timePeriod < 2.0 ) { //usually it take less than 0.5 sec to get a new location but you can use any value greater than 0.5 but i recommend 1.0 or 2.0 // process the location MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(newLocation.coordinate, self.mRadius, self.mRadius); [self setRegion:region animated:YES]; self.showsUserLocation = YES; self.userTrackingMode = YES; self.userLocation.title = @"oneRain"; [self StopLocating]; if (mDidLocatedCallback != NULL) { (*mDidLocatedCallback)(newLocation.coordinate.latitude, newLocation.coordinate.longitude); } } } } // Other information of location // Pin - (MKAnnotationView*) mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { MapAnnotationView* annotationView = nil; if (![annotation isKindOfClass:[MKUserLocation class]]) { annotationView = [[MapAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"player"]; // TODO classify by gender MapAnnotation* mapAnnotation = (MapAnnotation*) annotation; if (mapAnnotation.mIsMan) { annotationView.image = mMaleImage; } else { annotationView.image = mFemaleImage; } annotationView.annotation = annotation; UITapGestureRecognizer* tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(HandleTap:)]; [annotationView addGestureRecognizer:tapGestureRecognizer]; [tapGestureRecognizer release]; [mAnnoViewArr addObject:annotationView]; } return annotationView; } // Interface - (void)StartLocating { [self.mLocationManager startUpdatingLocation]; } - (void)StopLocating { [self.mLocationManager stopUpdatingLocation]; } - (void)AddNearbyPlayer:(PlayerInMap *)player { if (player == NULL) return; [mNearbyPlayers addObject:(id)player]; // Add two test location MapAnnotation* anno = [[[MapAnnotation alloc] InitWithCoordinate: player->mLocation] autorelease]; anno.mPlayerID = player->mPlayerID; anno.mIsMan = player->mIsMan; [self addAnnotation:anno]; [mAnnoArray addObject:anno]; } - (void)RemoveAllPlayers { [self removeAnnotations:mAnnoArray]; } // Handle tap event - (void)HandleTap:(UIGestureRecognizer *)recognizer { if ([recognizer.view isKindOfClass:[MapAnnotationView class]]) { if (mPreMapAnnotionView != nil) { MapAnnotation* preMapAnno = (MapAnnotation*) mPreMapAnnotionView.annotation; mPreMapAnnotionView.image = preMapAnno.mIsMan ? mMaleImage : mFemaleImage; } MapAnnotationView* annoView = (MapAnnotationView*)(recognizer.view); MapAnnotation* anno = (MapAnnotation*) annoView.annotation; annoView.image = anno.mIsMan ? mMaleSelectedImage : mFemaleSelectedImage; mPreMapAnnotionView = annoView; if (mSelectedPlayerCallback != NULL) { (*mSelectedPlayerCallback)(anno.mPlayerID); } } } @end
4.关联
接口和ios本地代码都写完了,剩下的就是将这两部分关联起来了,在ios平台下,我用CCMapViewIOS来实现CCMapView接口类,思想是对游戏提供调用接口,并通过调用ios本地代码来实现游戏想要的操作!
// // CCMapViewIOS.mm // Cocos2dxMap // // Created by quyou on 13-2-26. // // #include "CCMapView.h" #include "EAGLView.h" #include "MapView.h" #include "CCGeometry.h" #include "CCDirector.h" #define GetMapViewImpl ((MapView*)mMapView) CCMapView::CCMapView(cocos2d::CCRect const& frame) { mMapView = [[MapView alloc] initWithFrame:CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height)]; setAnchorPoint(cocos2d::CCPointMake(0.5f, 0.5f)); setContentSize(cocos2d::CCSizeMake(frame.size.width, frame.size.height)); [[EAGLView sharedEGLView] addSubview:GetMapViewImpl]; } CCMapView::~CCMapView() { } void CCMapView::StartLocating() { [GetMapViewImpl StartLocating]; } void CCMapView::StopLocating() { [GetMapViewImpl StopLocating]; } void CCMapView::SetRadius(float radius) { GetMapViewImpl.mRadius = radius; } void CCMapView::AddPlayer(const CCPlayerInMap &ccPlayerInMap) { PlayerInMap playerInMap = { ccPlayerInMap.mPlayerID, ccPlayerInMap.mIsMan, [NSString stringWithUTF8String:ccPlayerInMap.mPlayerName.c_str()], CLLocationCoordinate2DMake(ccPlayerInMap.mLatitude, ccPlayerInMap.mLongtitude)}; [GetMapViewImpl AddNearbyPlayer:&playerInMap]; } void CCMapView::RemoveAllPlayers() { [GetMapViewImpl RemoveAllPlayers]; } void CCMapView::SetPosition(const cocos2d::CCPoint &pos) { CGRect frame = GetMapViewImpl.frame; frame.origin.x = pos.x - getContentSize().width * getAnchorPoint().x; frame.origin.y = cocos2d::CCDirector::sharedDirector()->getWinSize().height - (pos.y + getContentSize().height * (1 - getAnchorPoint().y)); [GetMapViewImpl setFrame:frame]; } void CCMapView::SetDidLocatedCallback(DidLocatedCallback callback) { GetMapViewImpl.mDidLocatedCallback = callback; } void CCMapView::SetSelectedPlayerCallback(SelectedPlayerCallback callback) { GetMapViewImpl.mSelectedPlayerCallback = callback; }
剩余还有些零零碎碎的东西,比如自定制MapAnnotationView和MapAnnotation等等。。。