HarmonyOS分布式游戏手柄开发详解

发布于 2021-7-20 15:02
浏览
1收藏

1. 项目介绍

 

HarmonyOS的分布式能力可以方便地扩展虚拟终端,造就新的交互,体验新的场景。本篇codelab通过分布式能力,使手机在智慧屏附近即可迅速被虚拟成一个手柄终端,将智慧屏的交互扩展到手机,充分结合手机的多模输入和智慧屏的大屏优点,组成新的多人娱乐场景。
本应用分两部分,安装在手机上的手柄端程序和安装在大屏上的大屏端程序。

 说明:
为便于演示,本篇codelab所指大屏均使用支持HarmonyOS的手机代替。

 

启动大屏端程序进入游戏主页面,此时会弹出手柄设备列表选择框,在选择(最多选择两个手柄设备)手柄设备点击【确定】发起连接请求;手柄端收到连接请求亦会弹出设备列表选择框,选择对应大屏设备点击【确定】,此时大屏端与手柄端连接完成。手柄端可向大屏端发送指令。
主界面:

  • 手柄端点击【START】按钮,大屏端进入游戏中界面,此时会根据已连接的手柄创建对应数量的玩家飞机;敌机从屏幕顶端随机水平位置出现并向屏幕下方垂直移动;降落伞从屏幕顶端随机水平位置出现并向屏幕下方移动。
  • 手柄端点击【PAUSE】按钮,游戏暂停,点击【START】按钮继续游戏。

游戏中界面:

  • 手柄端可通过滑动摇杆按钮控制大屏端玩家飞机的飞行方向。
  • 点击绿色按钮可以发射子弹,当子弹与敌机发生碰撞后,对应玩家飞机得分数值加100,同时摧毁子弹与敌机。
  • 当玩家飞机与降落伞发生碰撞,对应屏幕左下角或右下角炸弹数量+1,同时销毁降落伞。
  • 点击黄色按钮可以释放炸弹(若对应玩家机炸弹数量不为0)清空屏幕敌机,根据屏幕中被清空的敌机数量N,计算对应玩家飞机的得分N * 100。
  • 当玩家飞机与敌机发生碰撞,若玩家飞机数量为2,则玩家飞机和敌机将同时被摧毁,此玩家飞机对应的手柄操作无效;若屏幕内玩家飞机均被摧毁,则游戏结束并跳转到游戏结束界面。

游戏结束界面:

  • 展示玩家最终得分。
  • 任一手柄端点击【PAUSE】按钮,大屏端跳转到游戏主界面;任一手柄端点击【START】按钮,大屏端跳转到游戏中界面。

效果图如下:
a) 大屏端选择手柄设备,发起连接;手柄被拉起,弹出设备选择框,选择大屏设备进行连接。

 

HarmonyOS分布式游戏手柄开发详解-开源基础软件社区

 

b) 游戏中,飞机移动、发射子弹、捡降落伞(获取触发大招的炸弹)。HarmonyOS分布式游戏手柄开发详解-开源基础软件社区

c) 游戏结束,大屏端显示所有玩家得分,手柄端显示自己得分。HarmonyOS分布式游戏手柄开发详解-开源基础软件社区

2. 环境准备

 

    • 安装DevEco Studio,详情请参考DevEco Studio下载
    • 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
      • 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
      • 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境

 说明:
由于分布式跨设备启动和连接需要获取真机设备ID,故此应用效果在模拟器上无法实现。
请准备至少2台开启了开发者模式的HarmonyOS手机,满足以下要求:


所使用设备均以相同的华为账号登录。
所使用设备接入相同WiFi热点或者各设备间已完成蓝牙配对操作。
看各设备的"设置 > 超级终端"界面,确认"我的在线设备"中包含所使用的设备。
请提前申请证书。

 

3. 手柄端代码结构解读

 

在本篇codelab中我们只对核心代码进行讲解,您可以在第10章下载完整代码。手柄端工程的代码结构如下图:

HarmonyOS分布式游戏手柄开发详解-开源基础软件社区

 

  • 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章下载完整代码。大屏端工程的代码结构如下图:HarmonyOS分布式游戏手柄开发详解-开源基础软件社区

  • 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. 手柄端功能

 

向大屏端发送指令
手柄端只是向大屏端发送指令,由大屏端实现业务逻辑。有如下指令:

  1. 点击绿色按钮,玩家飞机发送一个发射子弹的指令。
  2. 点击黄色按钮,释放大招,使屏幕上所有敌机爆炸销毁(需玩家飞机获得降落伞中炸药包,指令才可生效)。
  3. 点击【START】按钮,发送开始游戏的指令。
  4. 点击【PAUSE】按钮,发送暂停游戏的指令。
  5. 滑动摇杆,发送用于控制飞机飞行方向的指令。
    以点击绿色按钮为例,代码示例如下:
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源码
github源码

已于2021-7-20 15:05:08修改
2
收藏 1
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐