OpenHarmony游戏应用程序-实现的一个手柄游戏

介绍

本篇Codelab是基于TS扩展的声明式开发范式编程语言,以及OpenHarmony的分布式能力实现的一个手柄游戏。

说明: 本示例涉及使用系统接口,需要手动替换Full SDK才能编译通过。

完成本篇Codelab需要两台开发板,一台开发板作为游戏端,一台开发板作为手柄端,实现如下功能:

  • 游戏端呈现飞机移动、发射子弹等效果。
  • 游戏端分布式拉起手柄端FA。
  • 手柄端与游戏端建立连接,发送指令给游戏端,比如移动飞机,发射子弹和释放技能等。

最终效果图如下:

搭建OpenHarmony环境

完成本篇Codelab我们首先要完成开发环境的搭建,本示例以RK3568开发板为例,参照以下步骤进行:

  1. 获取OpenHarmony系统版本:标准系统解决方案(二进制)。

      以3.1版本为例:

2.搭建烧录环境。

  • 完成DevEco Device Tool的安装
  • 完成RK3568开发板的烧录

3.搭建开发环境。

  • 开始前请参考工具准备,完成DevEco Studio的安装和开发环境配置。
  • 开发环境配置完成后,请参考使用工程向导创建工程(模板选择“Empty Ability”),选择JS或者eTS语言开发。
  • 工程创建完成后,选择使用真机进行调测。

分布式组网

本章节以系统自带的音乐播放器为例(具体以实际的应用为准),介绍如何完成两台设备的分布式组网。

  1. 硬件准备:准备两台烧录相同的版本系统的RK3568开发板A、B。
  2. 开发板A、B连接同一个WiFi网络。

打开设置-->WLAN-->点击右侧WiFi开关-->点击目标WiFi并输入密码。

3.将设备A,B设置为互相信任的设备。

  • 找到系统应用“音乐”。

  • 设备A打开音乐,点击左下角流转按钮,弹出列表框,在列表中会展示远端设备的id。

  • 选择远端设备B的id,另一台开发板(设备B)会弹出验证的选项框。

  • 设备B点击允许,设备B将会弹出随机PIN码,将设备B的PIN码输入到设备A的PIN码填入框中。

  1. 配网完毕。

代码结构解读

  • HandleEtsOpenHarmony
  • GameEtsOpenHarmony

本篇Codelab只对核心代码进行讲解,首先介绍一下整个工程的代码结构:

└── HandleGameApplication
	│── GameEtsOpenHarmony
	│  
	└── HandleEtsOpenHarmony

其中HandleEtsOpenHarmony为手柄端工程代码,GameEtsOpenHarmony为游戏端工程代码。

HandleEtsOpenHarmony

  • MainAbility:存放应用主页面。
    • pages/index.ets:应用主页面。
    • common/images:存放图片资源的目录。
  • ServiceAbility:存放ServiceAbility相关文件。
    • service.ts:service服务,用于跨设备连接后通讯。

GameEtsOpenHarmony

  • MainAbility:存放应用主页面。
    • pages/index.ets:应用主页面。
    • common/images:存放图片资源。
  • model:存放获取组网内的设备列表相关文件。
    • RemoteDeviceModel.ets:获取组网内的设备列表。
    • GameElement.ets:游戏端界面元素的实体类,用于封装子弹、飞机等元素的属性。
  • ServiceAbility:存放ServiceAbility相关文件。
    • service.ts:service服务,用于跨设备连接后通讯。

实现手柄端功能

  1. 实现布局和样式。

手柄端有两个功能:向游戏端发送指令和实时获取游戏端得分数据。界面上有三个功能组件:蓝色图形组件用于控制游戏端飞机移动方向,黄色图形组件用于发射子弹,绿色图形组件用于释放技能,效果图如下:

主要代码如下:

@Entry
@Component
struct Index {
...
  build() {
    Stack() {
	...
		Text('score:' + this.score)
		...
      Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.SpaceBetween }) {
        Stack() {
          Image('/common/images/bigcircle.png')
            .width(300)
            .height(300)
          Image('/common/images/smallcircle.png')
            .width(140)
            .height(140)
            .position({ x: this.smallPosX, y: this.smallPosY }) // 30+75-35
        }
       ...
        Row() {
          Image('/common/images/a.png')
            .width(160)
            .height(160)
            .margin({ right: 20, bottom: 80 })
          Image('/common/images/b.png')
            .width(200)
            .height(200)
        }.alignItems(VerticalAlign.Bottom)
		...
    }
  }
}

2.实现摇杆功能。

给摇杆(蓝色小圆图形)添加TouchEvent,动态改变摇杆position属性使摇杆跟随手指移动,主要代码如下:

onTouchEvent(event: TouchEvent) {
  switch (event.type) {
    case TouchType.Down:
      this.startX = event.touches[0].screenX;
      this.startY = event.touches[0].screenY;
      break;
    case TouchType.Move:
      this.curX = event.touches[0].screenX;
      this.curY = event.touches[0].screenY;
      this.getSmallCurrentPos(this.curX - this.smallR - 60, this.curY - this.smallR - 60)
      angle = Math.round(this.calculateAngle());
      break;
    default:
      break;
  }
}

3.计算摇杆偏移角度。

主要代码如下:

calculateAngle() {
  var angle = 0
  var degree = Math.atan(this.getDisAbsY() / this.getDisAbsX()) * 180 / Math.PI
  var quadrant = this.quadrant();
  switch (quadrant) {
    case this.QUADRANT_1:
    // 向右上移动
      angle = degree;
      break;
    case this.QUADRANT_2:
    // 向左上移动
      angle = 180 - degree;
      break;
    case this.QUADRANT_3:
    // 向左下移动
      angle = -180 + degree;
      break;
    case this.QUADRANT_4:
    // 向右下移动
      angle = -degree;
      break;
    default:
      angle = 0;
      break;
  }
  return angle;
}

4.连接游戏端Service。

当手柄端被游戏端拉起时,获取游戏端传递的数据:游戏端deviceId和分数score。然后通过deviceId连接游戏端Service,主要代码如下:

aboutToAppear() {
  // 当被拉起时,通过want传递的参数同步对端界面UI
  await featureAbility.getWant((error, want) => {
    // 远端被拉起后,连接游戏端的service
    if (want.parameters.deviceId) {
      let remoteDeviceId = want.parameters.deviceId
      connectRemoteService(remoteDeviceId)
    }
  });
}

async function connectRemoteService(deviceId) {
...
  await featureAbility.connectAbility(
    {
      'deviceId': deviceId,
      'bundleName': "com.huawei.cookbook",
      'abilityName': "com.huawei.cookbook.ServiceAbility",
    },
    {
      onConnect: onConnectCallback,
      onDisconnect: onDisconnectCallback,
      onFailed: onFailedCallback,
    },
  );
}

5.通过RPC发送数据到游戏端。

连接游戏端Service之后,摇杆角度angle和操作类型actionType(1为发射子弹,2为释放技能)发送给游戏端,主要代码如下:

async function sendMessageToRemoteService() {
...
  let option = new rpc.MessageOption();
  let data = new rpc.MessageParcel();
  let reply = new rpc.MessageParcel();
  data.writeInt(actionType);
  data.writeInt(angle);
  await mRemote.sendRequest(1, data, reply, option);
}

实现游戏端功能

  1. 实现布局和样式。

游戏界面主要由玩家飞机、敌机、子弹和道具(降落伞)等组成,由于敌机和子弹都是多个的,所以使用ForEach来实现,主要代码如下:

@Entry
@Component
struct Index {

  build() {
    Stack() {
    ... 

      ForEach(this.bullets, item => {
        Image(item.imgSrc)
          .width(item.imgWidth)
          .height(item.imgHeight)
          .position({ x: item.positionX, y: item.positionY })
      }, item => item.timestamp.toString())

      ForEach(this.enemyPlanes, item => {
        Image(item.imgSrc)
          .width(item.imgWidth)
          .height(item.imgHeight)
          .position({ x: item.positionX, y: item.positionY })
      }, item => item.timestamp.toString())

      Image('/common/images/planeOne.png')
        .width(this.planeSize)
        .height(this.planeSize)
        .position({ x: this.planePosX, y: this.planePosY })
        .onTouch((event: TouchEvent) => {
          this.onTouchEvent(event)
        })

      Image('/common/images/props.png')
        .width(this.propsSize)
        .height(this.propsSize)
        .position({ x: this.propsPosX, y: this.propsPosY })
    ...
    }
    .height('100%')
    .width('100%')
  }
}

2.实现游戏端元素动画效果。

飞机、子弹和道具等元素的移动是通过动态改变Image的position属性来实现的。使用定时器setInterval每隔16ms重新设置界面元素position属性的值,主要实现代码如下:

 startGame() {
    var that = this
    setInterval(function () {   
      // 每60*16ms创建一个敌机
      if (that.num % 60 == 0) {
        that.createEnemyPlane()
      }
      // 移动子弹
      var bulletsTemp: GameElement[] = []
      for (var i = 0; i < that.bullets.length; i++) {
        var bullet = that.bullets[i]
        bullet.positionY -= 8
        // 当子弹移除屏幕外的时候,释放掉
        if (bullet.positionY > 0) {
          bulletsTemp.push(bullet)
        }
      }
      that.bullets = bulletsTemp
      // 移动飞机
      var enemyPlanesTemp: GameElement[] = []
      for (var j = 0; j < that.enemyPlanes.length; j++) {
        var enemyPlane = that.enemyPlanes[j]
        enemyPlane.positionY += 6

        // 当飞机移除屏幕外的时候,释放掉
        if (enemyPlane.positionY < that.screenHeight) {
          enemyPlanesTemp.push(enemyPlane)
        }
      }
      that.enemyPlanes = enemyPlanesTemp
      // 每隔 500*16ms显示降落伞
      if (that.num % 500 == 0) {
        that.getPropsFlag = true
        that.propsPosY = -that.propsSize
        that.propsPosX = Math.round((Math.random() * (that.screenWidth - that.propsSize)))
      }
      // 刷新道具位置
      if (that.propsPosY < that.screenHeight) {
        that.propsPosY += 6
      }
      that.checkCollision()
    }, 16);
  }

3.判断元素是否发生碰撞。

在setInterval中改变元素位置的时候同时检测元素之间是否发生碰撞,子弹和敌机发生碰撞则分数值改变(摧毁小飞机加50分,摧毁大飞机加100分),玩家飞机和道具发生碰撞则道具加1,主要实现代码如下:

 checkCollision() {
 ...
    for (var i = 0; i < this.enemyPlanes.length; i++) {
      var enemy = this.enemyPlanes[i];
      for (var j = 0; j < this.bullets.length; j++) {
        var bullet = this.bullets[j];
        var inside = this.isInside(bullet, enemy);
        // 发生碰撞
        if (inside) {
          enemy.imgSrc = '/common/images/boom.png'
          if (enemy.flag == 1) {
            this.score += 50
            sendMessageToRemoteService(that.score)
          } else if (enemy.flag == 2) {
            this.score += 100
            sendMessageToRemoteService(that.score)
          }
          // 清除子弹
          this.enemyPlanes.splice(i, 1);
          i--;
          enemy.flag = 3
          // 清除被子弹打中敌机
          that.bullets.splice(j, 1);
          j--;
        }
      }
    }
    // 飞机和降落伞是否发生碰撞
    var isGetProps = this.isInside(myPlane, props);
    if (isGetProps && this.getPropsFlag) {
      this.getPropsFlag = false
      this.bombNum++
      this.propsPosY = 2000
    }
  }

4.获取设备列表。

点击界面右上角的“电脑”图标,调用registerDeviceListCallback()发现设备列表,并弹出设备列表选择框DeviceListDialog ,选择设备后拉起远端FA。DeviceListDialog 主要代码如下:

@CustomDialog
export struct DeviceListDialog {
  controller: CustomDialogController

  build() {
    Column() {
      Text("选择设备")
        .fontWeight(FontWeight.Bold)
        .fontSize(20)
        .margin({ top: 20, bottom: 10 })

      List() {
        ForEach(deviceList, item => {
          ListItem() {
            Stack() {
              Text(item)
                .fontSize(12)
                .margin({ top: 10 })
            }
            .onClick(() => {
              startRemoteAbility(item)
              this.controller.close();
            })
            .padding({ left: 30, right: 30 })
          }
        }, item => item.toString())
      }
      .height("30%")
      .align(Alignment.TopStart)
...
    }
  }
}

5.拉起手柄端FA。

点击设备列表获取远程设备id后,拉起手柄端FA,代码如下:

function startRemoteAbility(deviceId) {
  var params = {
    deviceId: localDeviceId
  }
  var wantValue = {
    bundleName: 'com.huawei.cookbook',
    abilityName: 'com.huawei.cookbook.MainAbility',
    deviceId: deviceId,
    parameters: params
  };
  featureAbility.startAbility({
    want: wantValue
  }).then((data) => {
    console.info('[game] featureAbility.startAbility finished, localDeviceId=' + localDeviceId + '----deviceId:' + deviceId);
    // 拉起远端后,连接远端service
    connectRemoteService(deviceId)
  });
}

6.连接手柄端Service。

拉起手柄端FA后,连接手柄端Service,代码如下:

async function connectRemoteService(deviceId) {
  // 连接成功的回调
  async function onConnectCallback(element, remote) {
    mRemote = remote;
  }
...
  if (remoteDeviceModel.deviceList.length === 0) {
    return;
  }
  await featureAbility.connectAbility(
    {
      'deviceId': deviceId,
      'bundleName': "com.huawei.cookbook",
      'abilityName': "com.huawei.cookbook.ServiceAbility",
    },
    {
      onConnect: onConnectCallback,
      onDisconnect: onDisconnectCallback,
      onFailed: onFailedCallback,
    },
  );
}

7.通过RPC发送数据到手柄端。

通过RPC将游戏分数发送给手柄端,主要代码如下:

async function sendMessageToRemoteService(score) {
  console.log('[game]connectRemoteService sendMessageToRemoteService:')
  if (mRemote == null) {
    return;
  }
  let option = new rpc.MessageOption();
  let data = new rpc.MessageParcel();
  let reply = new rpc.MessageParcel();
  data.writeInt(score);
  await mRemote.sendRequest(1, data, reply, option);
}

8.Service发布公共事件。

通过Service接收手柄端数据,然后使用CommonEvent模块将数据发送给FA,主要代码如下:

class GameServiceAbilityStub extends rpc.RemoteObject {
...
    onRemoteRequest(code, data, reply, option) {
        console.log('[game]Service onRemoteRequest');
        var publishCallBack;
        if (code === 1) {
            // 读取手柄端发送的数据
            let actionType = data.readInt();
            let angle = data.readInt();
            reply.writeInt(100);
            var params = {
                actionType: actionType,
                angle: angle,
            }
            var options = {
                code: 1,
                data: 'init data',
                isOrdered: true,
                bundleName: 'com.huawei.cookbook',
                parameters: params
            }
            publishCallBack = function () {}
            // 发布公共事件
            commonEvent.publish("publish_action", options, publishCallBack);
        } 
        return true;
    }
}

9.FA订阅公共事件。

订阅公共事件,接收从Service发送的公共事件数据,actionType 为操作类型(1表示发送子弹指令,2表示释放技能指令),angle 为飞机移动的角度。接收到数据后执行手柄端发送的指令:移动玩家飞机、发射子弹和释放技能摧毁所有敌机,主要代码如下:

subscribeEvent() {
...
  // 订阅公共事件回调
  function SubscribeCallBack(err, data) {
    let msgData = data.data;
    let code = data.code;
...
    // 处理接收到的数据data
    that.actionType = data.parameters.actionType;
    that.angle = data.parameters.angle;

    if (that.actionType == 1) {
      that.createBullet()
    }
    if (that.actionType == 2) {
      if (that.bombNum > 0) {
        that.bombNum--
        that.destroyAllEnemy()
      }
    }
    if (that.angle != 0) {
      that.movePlaneByHandle()
    }
  }
  //创建订阅者回调
  function CreateSubscriberCallBack(err, data) {
    subscriber = data;
    //订阅公共事件
    commonEvent.subscribe(subscriber, SubscribeCallBack);
  }
  //创建订阅者
  commonEvent.createSubscriber(subscribeInfo, CreateSubscriberCallBack);
}

恭喜您

通过本篇Codelab,您可以学到:

如何跨设备拉起远程FA。

如何连接远程Service。

使用RPC实现本地FA和远程Servcice通信。

通过CommonEvent发布与订阅实现Service和FA之间通信。

为了帮助大家更深入有效的学习到鸿蒙开发知识点,小编特意给大家准备了一份全套最新版的HarmonyOS NEXT学习资源,获取完整版方式请点击→《HarmonyOS教学视频

HarmonyOS教学视频

鸿蒙语法ArkTS、TypeScript、ArkUI等.....视频教程

鸿蒙生态应用开发白皮书V2.0PDF:

获取白皮书完整版方式请点击→鸿蒙生态应用开发白皮书V2.0PDF

鸿蒙 (Harmony OS)开发学习手册

一、入门必看

  1. 应用开发导读(ArkTS)
  2. ……

二、HarmonyOS 概念

  1. 系统定义
  2. 技术架构
  3. 技术特性
  4. 系统安全
  5. ........

三、如何快速入门?《做鸿蒙应用开发到底学习些啥?

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

四、开发基础知识

  1. 应用基础知识
  2. 配置文件
  3. 应用数据管理
  4. 应用安全管理
  5. 应用隐私保护
  6. 三方应用调用管控机制
  7. 资源分类与访问
  8. 学习ArkTS语言
  9. ……

五、基于ArkTS 开发

  1. Ability开发
  2. UI开发
  3. 公共事件与通知
  4. 窗口管理
  5. 媒体
  6. 安全
  7. 网络与链接
  8. 电话服务
  9. 数据管理
  10. 后台任务(Background Task)管理
  11. 设备管理
  12. 设备使用信息统计
  13. DFX
  14. 国际化开发
  15. 折叠屏系列
  16. ……

更多了解更多鸿蒙开发的相关知识可以参考:鸿蒙 (Harmony OS)开发学习手册

相关推荐

  1. 用Python写一个简单坦克大战游戏实例

    2024-03-23 05:16:08       13 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-23 05:16:08       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-23 05:16:08       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-23 05:16:08       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-23 05:16:08       20 阅读

热门阅读

  1. 图书管理借阅系统(SpringBoot项目)

    2024-03-23 05:16:08       20 阅读
  2. 【WPF应用7】 基本控件-Grid 布局的详解与示例

    2024-03-23 05:16:08       16 阅读
  3. 初识Golang,Golang 中的结构体和方法

    2024-03-23 05:16:08       18 阅读
  4. 2024年奥莱利科技趋势报告解析

    2024-03-23 05:16:08       16 阅读
  5. Redis+Lua脚本+SpringAOP实现接口限流

    2024-03-23 05:16:08       16 阅读
  6. 从汉字之源厘清易混淆的倍数问题

    2024-03-23 05:16:08       17 阅读
  7. C# 多态 weijiejue

    2024-03-23 05:16:08       15 阅读
  8. 使用conda管理python环境

    2024-03-23 05:16:08       15 阅读