HarmonyOS分布式游戏手柄开发详解
1. 项目介绍
HarmonyOS的分布式能力可以方便地扩展虚拟终端,造就新的交互,体验新的场景。本篇codelab通过分布式能力,使手机在智慧屏附近即可迅速被虚拟成一个手柄终端,将智慧屏的交互扩展到手机,充分结合手机的多模输入和智慧屏的大屏优点,组成新的多人娱乐场景。
本应用分两部分,安装在手机上的手柄端程序和安装在大屏上的大屏端程序。
说明:
为便于演示,本篇codelab所指大屏均使用支持HarmonyOS的手机代替。
启动大屏端程序进入游戏主页面,此时会弹出手柄设备列表选择框,在选择(最多选择两个手柄设备)手柄设备点击【确定】发起连接请求;手柄端收到连接请求亦会弹出设备列表选择框,选择对应大屏设备点击【确定】,此时大屏端与手柄端连接完成。手柄端可向大屏端发送指令。
主界面:
- 手柄端点击【START】按钮,大屏端进入游戏中界面,此时会根据已连接的手柄创建对应数量的玩家飞机;敌机从屏幕顶端随机水平位置出现并向屏幕下方垂直移动;降落伞从屏幕顶端随机水平位置出现并向屏幕下方移动。
- 手柄端点击【PAUSE】按钮,游戏暂停,点击【START】按钮继续游戏。
游戏中界面:
- 手柄端可通过滑动摇杆按钮控制大屏端玩家飞机的飞行方向。
- 点击绿色按钮可以发射子弹,当子弹与敌机发生碰撞后,对应玩家飞机得分数值加100,同时摧毁子弹与敌机。
- 当玩家飞机与降落伞发生碰撞,对应屏幕左下角或右下角炸弹数量+1,同时销毁降落伞。
- 点击黄色按钮可以释放炸弹(若对应玩家机炸弹数量不为0)清空屏幕敌机,根据屏幕中被清空的敌机数量N,计算对应玩家飞机的得分N * 100。
- 当玩家飞机与敌机发生碰撞,若玩家飞机数量为2,则玩家飞机和敌机将同时被摧毁,此玩家飞机对应的手柄操作无效;若屏幕内玩家飞机均被摧毁,则游戏结束并跳转到游戏结束界面。
游戏结束界面:
- 展示玩家最终得分。
- 任一手柄端点击【PAUSE】按钮,大屏端跳转到游戏主界面;任一手柄端点击【START】按钮,大屏端跳转到游戏中界面。
效果图如下:
a) 大屏端选择手柄设备,发起连接;手柄被拉起,弹出设备选择框,选择大屏设备进行连接。
b) 游戏中,飞机移动、发射子弹、捡降落伞(获取触发大招的炸弹)。
c) 游戏结束,大屏端显示所有玩家得分,手柄端显示自己得分。
2. 环境准备
-
- 安装DevEco Studio,详情请参考DevEco Studio下载。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
说明:
由于分布式跨设备启动和连接需要获取真机设备ID,故此应用效果在模拟器上无法实现。
请准备至少2台开启了开发者模式的HarmonyOS手机,满足以下要求:
所使用设备均以相同的华为账号登录。
所使用设备接入相同WiFi热点或者各设备间已完成蓝牙配对操作。
看各设备的"设置 > 超级终端"界面,确认"我的在线设备"中包含所使用的设备。
请提前申请证书。
3. 手柄端代码结构解读
在本篇codelab中我们只对核心代码进行讲解,您可以在第10章下载完整代码。手柄端工程的代码结构如下图:
- devices:设备列表适配器和设备选择对话框、权限动态申请。
- handle:手柄实体类,包含游戏暂停、重新开始以及技能是否被点击。
- proxy:定义了连接远程service(PA)实现类以及代理。
- service:远程通信,读取大屏端发过来的数据,即玩家得分。
- slice:MainAbilitySlice主界面,弹窗选择大屏设备后跳转到HandleAbilitySlice界面,HandleAbilitySlice实现了分布式设备连接和手柄界面。
- utils:CalculAngle处理摇杆滑动事件,包含计算偏移角度;Constans常量类。
- resources:存放工程使用到的资源文件,其中resources\base\layout下存放xml布局文件,resources\base\media下存放图片资源。
- config.json:应用的配置文件。建议使用SDK4,即config.json中的“target”字段和模块下的build.gradle中的“compileSdkVersion”字段修改为4。
4. 大屏端代码结构解读
本篇codelab中只对核心代码进行讲解,您可以在第10章下载完整代码。大屏端工程的代码结构如下图:
- devices:展示手柄设备列表对话框,用户选择手柄设备(最多选择两个手柄设备)进行连接。
- game:游戏中的各个角色类的定义以及游戏界面定义。
- service:远程通信,用于获取手柄端发送的数据。
- slice:游戏主界面、游戏中界面、游戏结束界面逻辑判断。
- util:工具类,Constants.java定义常量,GameUtils.java获取当前设备屏幕尺寸、将图片id转换成PixelMapHolder对象。
- GameOverAbility.java:游戏结束的Page Ability。
- MainAbility.java:游戏主界面的Page Ability,由DevEco Studio生成,不需添加逻辑。
- MyApplication.java:入口类,由DevEco Studio生成,不需添加逻辑。
- PlaneGameAbility.java:游戏中的Page Ability。
- resources:存放工程使用到的资源文件,其中resources\base\layout下存放xml布局文件,resources\base\media下存放图片资源。
- config.json:Ability声明以及权限配置。
5. 相关权限
手柄端和大屏端程序开发均需申请以下多设备协同相关的四个权限,应用权限的申请可以参考权限章节。
ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换。
ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化。
ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息。
ohos.permission.GET_BUNDLE_INFO:用于允许查询其他应用信息的权限。
说明:
其中多设备协同数据同步权限"ohos.permission.DISTRIBUTED_DATASYNC",需要按照动态申请流程向用户申请授权。
6. 分布式设备启动与连接
大屏端发起连接,进入应用,大屏端弹出手柄设备列表选择框。代码示例如下:
// 设备选择弹出框
private SelectDeviceDialog showDialog() {
return new SelectDeviceDialog(this, new SelectDeviceDialog.SelectResultListener() {
public void callBack(List deviceInfos) {
for (DeviceInfo deviceInfo : deviceInfos) {
Handle handleInfo = new Handle(MainAbilitySlice.this); // 连接信息
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId(deviceInfo.getDeviceId())
.withBundleName(getBundleName())
.withAbilityName(MainAbility.class.getName())
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
intent.setOperation(operation);
startAbility(intent);
// 保存手柄连接信息
boolean isConn = connectRemotePa(deviceInfo.getDeviceId(), 1, handleInfo);
handleInfo.setDeviceId(deviceInfo.getDeviceId());
handleInfo.setConn(isConn);
handles.add(handleInfo);
}
}
});
}
选择手柄设备(可多选),点击【确定】按钮,连接选择的手柄,代码示例如下:
private boolean connectRemotePa(String deviceId, int requestType, Handle handleInfo) {
Intent connectPaIntent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId(deviceId)
.withBundleName(getBundleName())
.withAbilityName(GameServiceAbility.class.getName())
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
connectPaIntent.setOperation(operation);
IAbilityConnection conn = new IAbilityConnection() {
@Override
public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {
GameRemoteProxy proxy = new GameRemoteProxy(remote);
handleInfo.setProxy(proxy);
}
@Override
public void onAbilityDisconnectDone(ElementName elementName, int i) {
if (handleInfo.getProxy() != null) {
handleInfo.setProxy(null);
}
}
};
boolean ret = connectAbility(connectPaIntent, conn);
return ret;
}
大屏端发起连接后,手柄端被远程拉起,此时手柄端弹出设备列表选择框,选择大屏设备,点击【确定】按钮,连接选择的大屏。代码示例如下:
private boolean connectRemotePa(String deviceId, int requestType) {
Intent connectPaIntent = new Intent();
Operation operation = new Intent.OperationBuilder().withDeviceId(deviceId)
.withBundleName(getBundleName())
.withAbilityName(GameServiceAbility.class.getName())
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
connectPaIntent.setOperation(operation);
IAbilityConnection conn = new IAbilityConnection() {
@Override
public void onAbilityConnectDone(ElementName elementName, IRemoteObject remote, int resultCode) {
proxy = new GameRemoteProxy(remote, localDeviceId, calculAngle, handle);
if (proxy != null) {
try {
proxy.senDataToRemote(requestType);
} catch (RemoteException e) {
LogUtil.error(TAG, "onAbilityConnectDone RemoteException");
}
}
}
@Override
public void onAbilityDisconnectDone(ElementName elementName, int index) {
if (proxy != null) {
proxy = null;
}
}
};
boolean ret = connectAbility(connectPaIntent, conn);
return ret;
}
7. 手柄端功能
向大屏端发送指令
手柄端只是向大屏端发送指令,由大屏端实现业务逻辑。有如下指令:
- 点击绿色按钮,玩家飞机发送一个发射子弹的指令。
- 点击黄色按钮,释放大招,使屏幕上所有敌机爆炸销毁(需玩家飞机获得降落伞中炸药包,指令才可生效)。
- 点击【START】按钮,发送开始游戏的指令。
- 点击【PAUSE】按钮,发送暂停游戏的指令。
- 滑动摇杆,发送用于控制飞机飞行方向的指令。
以点击绿色按钮为例,代码示例如下:
private Component.ClickedListener listenerA = new Component.ClickedListener() {
@Override
public void onClick(Component component) {
vibrator(Constants.VIBRATION_30); // 震动
handle.setIsAbtnClick(1);
isFlagA = true;
if (isConn && proxy != null) {
try {
proxy.senDataToRemote(1);
} catch (RemoteException e) {
LogUtil.error(TAG, "Send Data to Remote Failed...");
}
}
handle.setIsAbtnClick(0);
}
};
计算偏移角度
摇杆部分,大圆是固定不动的,小圆默认停在大圆中间。手指移动时,小圆跟随手指的位置移动但不能超出大圆范围。以大圆圆心为坐标系原点,手指的坐标点与X轴形成的角度为玩家飞机的偏移角度,以此来控制飞机行进方向。关键代码示例如下:
1.计算手指所在象限
private boolean getFlagX() {
return 0 < moveX - startPosX ? true : false;
}
private boolean getFlagY() {
return 0 < moveY - startPosY ? true : false;
}
// 返回手指所在象限(坐标原点为大圆圆心)
private int quadrant() {
if (getFlagX() && !getFlagY()) {
return Constants.QUADRANT_1;
} else if (!getFlagX() && !getFlagY()) {
return Constants.QUADRANT_2;
} else if (!getFlagX() && getFlagY()) {
return Constants.QUADRANT_3;
} else if (getFlagX() && getFlagY()) {
return Constants.QUADRANT_4;
} else {
return 0;
}
}
2.计算偏移角度
private int calculateAngle() {
int degree = (int) Math.toDegrees(Math.atan(getDisAbsY() / getDisAbsX()));
int quadrant = quadrant();
switch (quadrant) {
case Constants.QUADRANT_1:
// 向右上移动
angle = degree;
break;
case Constants.QUADRANT_2:
// 向左上移动
angle = Constants.DEGREE_180 - degree;
break;
case Constants.QUADRANT_3:
// 向左下移动
angle = -Constants.DEGREE_180 + degree;
break;
case Constants.QUADRANT_4:
// 向右下移动
angle = -degree;
break;
default:
angle = 0;
break;
}
return angle;
}
设置小圆坐标
当手指在大圆范围内时,小圆圆心坐标跟随手指坐标;当手指滑出大圆范围后,小圆圆心坐标在大圆圆周上,不能超出大圆范围。代码示例如下:
private float[] getSmallCurrentPos(float currX, float currY) {
float[] smallCurrentPos = new float[Constants.QUADRANT_2];
if (getDisZ() < bigR) {
smallCurrentPos[0] = currX;
smallCurrentPos[1] = currY;
return smallCurrentPos;
} else {
// 手指滑出大圆外后,由于小圆不能超出大圆,此时小圆圆心坐标在大圆圆周上,以下是计算小圆的控件坐标
double disX = (getDisAbsX() * bigR) / getDisZ();
double disY = (getDisAbsY() * bigR) / getDisZ();
int quadrant = quadrant(); // 手指所在象限
switch (quadrant) {
case Constants.QUADRANT_1:
smallCurrentPos[0] = (float) (disX + startPosX - smallR);
smallCurrentPos[1] = (float) (startPosY - disY - smallR);
break;
case Constants.QUADRANT_2:
smallCurrentPos[0] = (float) (startPosX - disX - smallR);
smallCurrentPos[1] = (float) (startPosY - disY - smallR);
break;
case Constants.QUADRANT_3:
smallCurrentPos[0] = (float) (startPosX - disX - smallR);
smallCurrentPos[1] = (float) (disY + startPosY - smallR);
break;
case Constants.QUADRANT_4:
smallCurrentPos[0] = (float) (disX + startPosX - smallR);
smallCurrentPos[1] = (float) (disY + startPosY - smallR);
break;
default:
break;
}
}
return smallCurrentPos;
}
显示得分
手柄端接收大屏端发送的数据,更新UI组件,代码示例如下:
public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
int score = data.readInt();
txtScore.setText(String.valueOf(score));
return true;
}
8. 大屏端功能
通过获取手柄端发送的数据操作大屏端:开始游戏、暂停游戏、移动飞机以及发射子弹等。大屏端可随机生成敌机、降落伞以及检测碰撞事件。
精灵类定义
1.子类公共属性定义,代码示例如下:
private PixelMapHolder pixelMapHolder; // 图片
private int planeX; // x坐标
private int planeY; // y坐标
private int width; // 宽
private int height; // 高
private int speed; // 速度
private boolean destroyed; // 是否被销毁
2.当前对象与其他对象集合的碰撞检测,如未发生碰撞返回null,否则返回碰撞对象,代码示例如下:
public Spirit collideWithOther(List<? extends Spirit> spirits) {
Iterator<? extends Spirit> iterator = spirits.iterator();
while (iterator.hasNext()) {
Spirit spirit = iterator.next();
RectFloat r1 = new RectFloat(spirit.getPlaneX(), spirit.getPlaneY(),
spirit.getPlaneX() + spirit.getWidth(),
spirit.getPlaneY() + spirit.getHeight());
RectFloat r2 = new RectFloat(planeX, planeY, planeX + width,
planeY + height);
if (r1.getIntersectRect(r2)) { // 碰撞
return spirit;
}
}
return optionalEmpty.orElse(null);
}
3.画图方法,包含画图前、画图中、画图后三个方法,代码示例如下:
public void draw(Canvas canvas, Paint paint) {
beforeOndraw();
onDraw(canvas, paint);
afterDraw(canvas, paint);
}
4.画图前执行的方法,计算精灵类垂直方向坐标,代码示例如下:
public void beforeOndraw() {
move();
}
public void move() {
planeY += speed;
}
5.画图前执行的方法,计算精灵类垂直方向坐标,代码示例如下:
public void onDraw(Canvas canvas, Paint paint) {
canvas.drawPixelMapHolder(pixelMapHolder, planeX, planeY, paint);
}
6.画图之后执行的方法,销毁移出屏幕的精灵对象,代码示例如下:
public void afterDraw(Canvas canvas, Paint paint) {
if (GameUtils.getScreenHeight() < planeY) {
destroy();
}
}
7.执行销毁方法,将图片pixelMapHolder对象设置为null,将销毁状态设置为true,代码示例如下:
public void destroy() {
pixelMapHolder = null;
destroyed = true;
}
精灵类子类定义
1.Bomb.java降落伞类,构造方法中设置水平方向随机位置、垂直方向位置和速度,代码示例如下:
public Bomb(PixelMapHolder pixelMapHolder) {
super(pixelMapHolder);
this.setPlaneX(getRandom().nextInt(GameUtils.getScreenWidth() - BOMB_Y_POSITION));
this.setPlaneY(-BOMB_Y_POSITION);
this.setSpeed((getRandom().nextInt(SPEED_RATE) + SPEED_RATE) * SPEED_RATE);
}
2.Bullet.java子弹类,构造函数中根据玩家飞机位置设置子弹位置以及设置速度;重写afterDraw()方法,用于销毁超出屏幕范围的子弹,代码示例如下:
public Bullet(PixelMapHolder pixelMapHolder, int planeX, int planeY) {
super(pixelMapHolder);
this.setPlaneX(planeX + BULLET_X_OFFSET);
this.setPlaneY(planeY - BULLET_Y_OFFSET);
this.setSpeed(BULLET_SPEED);
}
public void afterDraw(Canvas canvas, Paint paint) {
if (getPlaneY() < 0) {
destroy();
}
}
3.EnemyPlane.java敌机类,构造函数中设置敌机尺寸大小、水平方向随机位置、垂直方向位置和随机速度,代码示例如下:
public EnemyPlane(PixelMapHolder pixelMapHolder, int planeIndex) {
super(pixelMapHolder);
int size = (planeIndex == SPEED_RATE) ? Constants.SMALL_PLANE_SIZE : Constants.BIG_PLANE_SIZE;
this.setPlaneX(getRandom().nextInt(GameUtils.getScreenWidth() - size));
this.setPlaneY(-size);
this.setSpeed((getRandom().nextInt(SPEED_RATE) + SPEED_RATE) * SPEED_RATE);
}
4.Explosion.java爆炸效果类,销毁精灵子类时展示爆炸效果,构造方法中设置爆炸位置(为被销毁对象的位置)、速度为0;重写afterDraw()方法,每8帧销毁爆炸效果,代码示例如下:
public Explosion(PixelMapHolder pixelMapHolder, int planeX, int planeY) {
super(pixelMapHolder);
this.setPlaneX(planeX);
this.setPlaneY(planeY);
this.setSpeed(0);
}
@Override
public void afterDraw(Canvas canvas, Paint paint) {
if (0 == GameView.getFrame() % CLEAN_DESTROY_FRAME) {
destroy();
}
}
5.MyPlane.java玩家飞机类,构造方法中设置水平、垂直方向位置、速度、分数为0、炸弹数为0;重写draw()方法,在屏幕上画玩家飞机,代码示例如下:
public MyPlane(PixelMapHolder pixelMapHolder, int planeX, int planeY) {
super(pixelMapHolder);
this.setPlaneX(planeX);
this.setPlaneY(planeY);
this.setSpeed(SPEED);
this.setScore(0);
this.setBombNum(0);
}
@Override
public void draw(Canvas canvas, Paint paint) {
canvas.drawPixelMapHolder(getPixelMapHolder(), getPlaneX(), getPlaneY(), paint);
}
获取手柄端数据
1.GameserviceAbility.java类中实时获取手柄端数据,调用Handle类处理数据,代码示例如下:
@Override
public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
String deviceId = data.readString();
int angle = data.readInt();
int buttonA = data.readInt();
int buttonB = data.readInt();
int pause = data.readInt();
int start = data.readInt();
taskDispatcher.syncDispatch(new Runnable() {
@Override
public void run() {
for (Handle handle: MainAbilitySlice.getHandles()) {
if (deviceId.equals(handle.getDeviceId())) {
handle.operation(angle, buttonA, buttonB, pause, start);
break;
}
}
}
});
return true;
}
2.Handle.java类,根据获取的手柄端数据和GameView.java类中的status变量的值对游戏进行操作,代码示例如下:
public void operation(int angle, int buttonA, int buttonB, int pause, int start) {
switch (GameView.getStatus()) {
case Constants.GAME_UNREADY: // 0游戏未开始
if (start != 0) {
startGame(); // 开始游戏
GameView.setStatus(Constants.GAME_START);
}
break;
case Constants.GAME_START: // 1游戏进行中
if (angle != 0) {
movePlane(angle); // 移动飞机
}
if (buttonA != 0) {
createBullet(); // 发射子弹
}
if (buttonB != 0) {
useBomb(); // 使用炸弹(清空屏幕敌机)
}
if (pause != 0) {
pause(); // 暂停
}
break;
case Constants.GAME_PAUSE: // 2游戏暂停
if (start != 0) { // 取消暂停
reStart();
GameView.setStatus(Constants.GAME_START);
}
break;
case Constants.GAME_STOP: // 3游戏结束
if (pause != 0) { // 返回到主页
quitGame();
GameView.setStatus(Constants.GAME_UNREADY);
}
if (start != 0) { // 重新开始游戏
startGame();
GameView.setStatus(Constants.GAME_START);
}
break;
default:
break;
}
}
生成玩家飞机
1.当GameView.getStatus()的值为0,且获取手柄的start参数不为0时,进入游戏界面,代码示例如下:
private void startGame() {
Intent intent = new Intent();
ElementName element = new ElementName("", abilitySlice.getBundleName(),
abilitySlice.getBundleName() + ".PlaneGameAbility");
intent.setElement(element);
abilitySlice.startAbility(intent);
}
2.进入游戏界面后,根据获取的手柄设备id集合创建玩家飞机,代码示例如下:
public void start(List<String> deviceIdList) {
this.deviceIds = deviceIdList;
this.myPlanes = new ArrayList<>(deviceIds.size());
setPixelMapHolder();
// 创建飞机
createMyPlane();
}
private void createMyPlane() {
int index = 0;
MyPlane myPlane = null;
int position = this.screenWidth / MAXMYPLANENUM - MY_PLANE_INITIAL_POSITION;
for (String deviceId : deviceIds) {
if (index == 0) {
myPlane = new MyPlane(myPlaneOnePixelMapHolder, position + index * MY_PLANE_INITIAL_POSITION,
this.screenHeight - MY_PLANE_V_LIMIT);
} else {
myPlane = new MyPlane(myPlaneTwoPixelMapHolder, position + index * MY_PLANE_INITIAL_POSITION,
this.screenHeight - MY_PLANE_V_LIMIT);
}
myPlane.setDeviceId(deviceId);
myPlane.setIndex(index);
InnerEvent event = InnerEvent.get(1, 0, myPlane);
myEventHandler.sendEvent(event);
myPlanes.add(myPlane);
index++;
}
}
3.画玩家飞机,代码示例如下:
private void drawMyPlane() {
Iterator<MyPlane> iterator = myPlanes.iterator();
while (iterator.hasNext()) {
MyPlane myPlane = iterator.next();
if (!myPlane.isDestroyed()) {
myPlane.draw(nowCanvas, paint);
}
}
}
移动玩家飞机
1.根据获取到的手柄端数据,移动玩家飞机,代码示例如下:
public void movePlaneByHandles(int angle, String deviceId) {
MyPlane myPlane = null;
for (MyPlane nowMyPlane : myPlanes) {
if (nowMyPlane.getDeviceId().equals(deviceId)) {
myPlane = nowMyPlane;
}
}
if (myPlane == null) {
return;
}
myPlane.setPlaneX(myPlane.getPlaneX()
+ ((int) (myPlane.getSpeed() * Math.cos(angle * (Math.PI / ANGULAR_180)))));
myPlane.setPlaneY(myPlane.getPlaneY()
- ((int) (myPlane.getSpeed() * Math.sin(angle * (Math.PI / ANGULAR_180)))));
if (myPlane.getPlaneX() < 0) {
myPlane.setPlaneX(0);
}
if (myPlane.getPlaneX() > this.screenWidth - MY_PLANE_H_LIMIT) {
myPlane.setPlaneX(this.screenWidth - MY_PLANE_H_LIMIT);
}
if (myPlane.getPlaneY() < 0) {
myPlane.setPlaneY(0);
}
if (myPlane.getPlaneY() > this.screenHeight - MY_PLANE_V_LIMIT) {
myPlane.setPlaneY(this.screenHeight - MY_PLANE_V_LIMIT);
}
}
发射子弹
1.根据获取到的手柄端数据,创建子弹,子弹初始位置根据玩家飞机位置确定,代码示例如下:
private void createBullet(MyPlane myPlane) {
if (myPlane == null) {
return;
}
Bullet bullet = new Bullet(bulletPixelMapHolder, myPlane.getPlaneX(), myPlane.getPlaneY());
bullet.setDeviceId(myPlane.getDeviceId());
bullets.add(bullet);
}
2.在屏幕中画子弹,销毁超出屏幕范围的子弹,并从子弹集合中移除,代码示例如下:
private void drawBullet() {
Iterator<Bullet> iterator = bullets.iterator();
while (iterator.hasNext()) {
Bullet bullet = iterator.next();
if (!bullet.isDestroyed()) {
bullet.draw(nowCanvas, paint);
} else {
iterator.remove();
}
}
}
使用炸弹
1.通过获取手柄端的设备ID获得对应的玩家飞机,若该玩家飞机炸弹数大于0,则销毁屏幕中所有敌机(敌机数为N),该玩家飞机加N * 100分,并返回分数到手柄端,代码示例如下:
public void bombEnemyPlaneByDeviceId(String deviceId) {
MyPlane myPlane = getMyPlaneByDeviceId(deviceId);
if (myPlane == null) {
return;
}
if (myPlane.getBombNum() == 0) {
return;
}
bombEnemyPlane(myPlane);
}
// 使用炸弹摧毁敌机
private void bombEnemyPlane(MyPlane myPlane) {
int score = 0; // 炸弹摧毁屏幕敌机获得分数
myPlane.setBombNum(myPlane.getBombNum() - 1);
Iterator<EnemyPlane> iteratorEnemyPlane = enemyPlanes.iterator();
while (iteratorEnemyPlane.hasNext()) {
EnemyPlane enemyPlane = iteratorEnemyPlane.next();
if (!enemyPlane.isDestroyed()) {
createExposion(enemyPlane.getPlaneX(), enemyPlane.getPlaneY());
score += SCORE;
enemyPlane.destroy();
}
}
myPlane.setScore(myPlane.getScore() + score);
InnerEvent event = InnerEvent.get(1, 0, myPlane);
myEventHandler.sendEvent(event);
}
随机生成敌机
1.随机生成敌机从屏幕上方任意水平位置出现,代码示例如下:
private void createEnemyPlane() {
// 随机生成敌机
int planeIndex = random.nextInt(SPEED_RATE) + SPEED_RATE;
PixelMapHolder enemyPlanePixelMapHolder = planeIndex == SMALL_ENEMY_PLANE_INDEX
? smallEnemyPlanePixelMapHolder : bigEnemyPlanePixelMapHolder;
EnemyPlane enemyPlane = new EnemyPlane(enemyPlanePixelMapHolder, planeIndex);
enemyPlanes.add(enemyPlane);
}
2.画敌机(每50帧生成一个敌机),销毁超出屏幕范围的敌机,并从敌机集合中移除,代码示例如下:
private void drawEnemyPlane() {
// 随机生成敌机
if (0 == frame % CREATE_ENEMY_PLANE_FRAME) {
createEnemyPlane();
}
Iterator<EnemyPlane> iterator = enemyPlanes.iterator();
while (iterator.hasNext()) {
EnemyPlane enemyPlane = iterator.next();
if (!enemyPlane.isDestroyed()) {
enemyPlane.draw(nowCanvas, paint);
} else {
iterator.remove();
}
}
}
随机生成降落伞
1.随机生成降落伞从屏幕上方任意水平位置出现,代码示例如下:
private void createBomb() {
// 随机生成炸弹
Bomb bomb = new Bomb(bulletAwardPixelMapHolder);
bombs.add(bomb);
}
2.画降落伞(每2000帧生成一个降落伞),销毁超出屏幕范围的降落伞,并从降落伞集合中移除,代码示例如下:
private void drawBomb() {
// 随机生成炸弹
if (0 == frame % CREATE_BOMB_FRAME) {
frame = 0;
createBomb();
}
Iterator<Bomb> iterator = bombs.iterator();
while (iterator.hasNext()) {
Bomb bomb = iterator.next();
if (!bomb.isDestroyed()) {
bomb.draw(nowCanvas, paint);
} else {
iterator.remove();
}
}
}
碰撞检测
1.子弹碰撞敌机后,销毁该子弹和敌机,子弹对应的玩家飞机加100分,并返回分数到手柄端,代码示例如下:
private void destroyEnemyPlane() {
MyPlane myPlane = null;
Iterator<Bullet> iterator = bullets.iterator();
while (iterator.hasNext()) {
Bullet bullet = iterator.next();
for (MyPlane nowMyPlane : myPlanes) {
if (nowMyPlane.getDeviceId().equals(bullet.getDeviceId())) {
myPlane = nowMyPlane;
break;
}
}
if (!bullet.isDestroyed() && (myPlane != null)) {
EnemyPlane enemyPlane = null;
Spirit spirit = bullet.collideWithOther(enemyPlanes);
if (spirit instanceof EnemyPlane) {
enemyPlane = (EnemyPlane) spirit;
}
if (enemyPlane != null) {
createExposion(enemyPlane.getPlaneX(), enemyPlane.getPlaneY());
iterator.remove();
enemyPlanes.remove(enemyPlane);
myPlane.setScore(myPlane.getScore() + SCORE);
InnerEvent event = InnerEvent.get(1, 0, myPlane);
myEventHandler.sendEvent(event);
}
}
}
}
2.玩家飞机碰撞敌机后,玩家飞机和敌机被销毁,若玩家飞机数量为0,则结束游戏,代码示例如下:
private void destroyMyPlane() {
Iterator<MyPlane> iterator = myPlanes.iterator();
while (iterator.hasNext()) {
MyPlane myPlane = iterator.next();
if (!myPlane.isDestroyed()) {
EnemyPlane enemyPlane = null;
Spirit spirit = myPlane.collideWithOther(enemyPlanes);
if (spirit instanceof EnemyPlane) {
enemyPlane = (EnemyPlane) spirit;
}
if (enemyPlane != null) {
createExposion(myPlane.getPlaneX(), myPlane.getPlaneY());
createExposion(enemyPlane.getPlaneX(), enemyPlane.getPlaneY());
enemyPlanes.remove(enemyPlane);
myPlane.setBombNum(0);
myPlane.destroy();
if (myPlanes.size() == MAXMYPLANENUM) {
iterator.remove();
} else {
status = Constants.GAME_STOP; // 游戏结束标识
}
}
}
}
}
3.降落伞碰撞玩家飞机后,销毁该降落伞,玩家飞机对应左/右下角炸弹数量加1,代码示例如下;
private void gainBomb() {
Iterator<Bomb> iterator = bombs.iterator();
while (iterator.hasNext()) {
Bomb bomb = iterator.next();
if (!bomb.isDestroyed()) {
MyPlane myPlane = null;
Spirit spirit = bomb.collideWithOther(myPlanes);
if (spirit instanceof MyPlane) {
myPlane = (MyPlane) spirit;
}
if (myPlane != null) {
myPlane.setBombNum(myPlane.getBombNum() + 1);
bomb.destroy();
iterator.remove();
}
}
}
}
返回分数
1.创建线程并绑定事件,返回分数到手柄端,代码示例如下:
// 新增创建新线程
myEventHandler = new MyEventHandler(EventRunner.create(true));
// 设置投递事件
InnerEvent event = InnerEvent.get(1, 0, myPlane);
myEventHandler.sendEvent(event);
// 创建EventHandler类
private class MyEventHandler extends EventHandler {
MyEventHandler(EventRunner runner) throws IllegalArgumentException {
super(runner);
}
@Override
protected void processEvent(InnerEvent event) {
super.processEvent(event);
int eventId = event.eventId;
if (eventId == 1) {
if (event.object instanceof MyPlane) {
MyPlane myPlane = (MyPlane) event.object;
if (myPlane.getIndex() == 0) {
playerOneScore = myPlane.getScore();
} else {
playerTwoScore = myPlane.getScore();
}
MainAbilitySlice.returnScore(myPlane.getScore(), myPlane.getDeviceId());
}
}
}
}
2.根据设备ID将分数返回到对应的手柄端,代码示例如下:
public static void returnScore(int score, String deviceId) {
for (Handle handleInfo: handles) {
try {
if (handleInfo.getDeviceId().equals(deviceId) && handleInfo.isConn() && handleInfo.getProxy() != null) {
handleInfo.getProxy().senDataToRemote(1, score);
break;
}
} catch (RemoteException e) {
HiLog.error(TAG, handleInfo.getDeviceId() + "::GameServiceAbility::returnScore faild");
}
}
}
游戏结束
1.屏幕中所有玩家飞机被摧毁后,游戏结束(GameView.status值为3),调用Handles类并传递玩家飞机最终得分,代码示例如下:
private void gameOver() {
MainAbilitySlice.getHandles().get(0).gameOver(playerOneScore, playerTwoScore);
}
2.Handles类跳转到游戏结束界面,并传递玩家飞机最终得分,代码示例如下:
public void gameOver(int playerOneScore, int playerTwoScore) {
Intent intent = new Intent();
intent.setParam("playerOneScore", playerOneScore);
intent.setParam("playerTwoScore", playerTwoScore);
ElementName element = new ElementName("", abilitySlice.getBundleName(),
abilitySlice.getBundleName() + ".GameOverAbility");
intent.setElement(element);
abilitySlice.startAbility(intent);
GameView.setStatus(Constants.GAME_STOP);
}
3.游戏结束界面展示玩家飞机得分,代码示例如下:
private static final String KEYONE = "playerOneScore";
private static final String KEYTWO = "playerTwoScore";
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_game_over);
// 获得玩家分数
Object scoreOne = intent.getParams().getParam(KEYONE);
Object scoreTwo = intent.getParams().getParam(KEYTWO);
// 设置分数
Component componentOne = findComponentById(ResourceTable.Id_playerOne);
Component componentTwo = findComponentById(ResourceTable.Id_playerTwo);
if (componentOne instanceof Text) {
Text textOne = (Text) componentOne;
textOne.setText("玩家一分数:" + scoreOne);
}
if (componentTwo instanceof Text) {
Text textTwo = (Text) componentTwo;
textTwo.setText("玩家二分数:" + scoreTwo);
}
}
9. 恭喜您
通过本篇codelab,您可以学到:
- 常用布局和常用组件
- 分布式设备启动与连接
- 线程间通信
- 利用canvas组件绘制图形
10. 参考
你好,gitee源码失效