【Android游戏开发详细过程2】Android平台飞机大战游戏APP设计与实现


前言

本系统是基于以上主流开发工具之一的 Android Studio 软件进行客户端的开发, 利用 Eclipse2017 软件实现服务器端开发,所用语言皆为 Java 语言。通过上述软件开发一款童年经典的小游戏: 飞机大战小游戏。


一、界面设计与功能实现

【Android游戏开发详细过程1】Android平台飞机大战游戏APP设计与实现

二、数据库设计与实现

2.1 Room数据库的好处

Room 因其良好的开发经验大大降低了 SQLite 的门槛。与 sqliteopenhelper 等传统方法相比,使用 room 驱动 SQLite 具有以下好处:
(1) API 设计友好, 使人容易理解。
(2) 编译期的 SQL 语法检查。
(3) 开发效率高,避免了大量的模板代码。
(4) 可以和 RxJava、 LiveData 等进行桥接。

2.2 Room的配置

在 Gradle scripts 下的第二个 build.gradle 下添加依赖如下。

implementation "androidx.room:room-runtime:2.4.2"
annotationProcessor"androidx.room:room-compiler:2.4.2"

在项目中创建一个新目录 room,在里面添加表,数据库,和数据库访问者。

2.3 表的设计

(1) 项目要用到的表有用户表 User,里面的属性如表 3.2 所示;飞机商店表ShopPlane,里面的属性如表格 3.3所示;用户飞机表 MyPlane,里面的属性如表格 3.4 所示。
在这里插入图片描述(2) 多对多关系中, 主表中的一条数据映射着从表中的零个或多个, 反之一样。每个 User 中可以有很多的商店飞机(ShopPlane),每个商店的飞机可以归属于不同的用户(User), 因此, User 和 ShopPlane 之间时多对多的关系。
定义两个表的联合表 MyPlane 关键代码如下:

@Entity(primaryKeys = {"planeId","userId"},
foreignKeys ={@ForeignKey(entity = User.class,parentColumns = "userId",child Columns = "userId"),
@ForeignKey(entity = ShopPlane.class,parentColumns = "planeId",childColum ns = "planeId")} )//在定义类名的前面书写这些代码。

多键对多键关系中, User表之间与 ShopPlane表之间并不是一个确定的外键的约束连接关系, 必须先建立起一种交叉的连接关系表: MyPlane 表, 然后再依次分别为 User 表之间与 ShopPlane 表之间产生一个有约束连接的外键。 User表与 ShopPlane 表之间的笛卡尔积分是数据交叉与连接的一个结果,即是二表数据的联合。

2.4 Room的管理与应用

(1) 它主要包括三个组件如下表 3.5 所示:
在这里插入图片描述
(2) 三个组件的关系图如图 3.15 所示。
在这里插入图片描述(3) 数据库(Database)是我们对底层数据库的访问, 创建 UserDatabase 关键代码如下:

@Database(entities ={User.class,MP.class,ShopPlane.class},version = 1,exportS chema = false)//在类名前定义, 异步开启数据库:
INSTANCE = Room.databaseBuilder
(context.getApplicationContext(),UserDatabase.class,"user_database")
.build();

( 4) 实体表示数据库中的表。我们使用@entity 来定义一个实体类,类中的属性对应于表中的列;所有属性必须是公共的,或者具有 get 或 set 方法;属性中至少有一个主键。使用@primarykey 表示一个主键,或定义多个主键,如下所示:当主键值为空时, autogenerate 能够使得在@primarykey( autogenerate=true)下自动生成键值。
( 5) DAO( Data Access Object) 提供了访问数据库的接口, DAO 的方法使用需要在当前线程进行, 因此要免于在 UI 线程直接使用。通过@Dao 定义 DAO 类,使用@Insert、 @Query 、 @Delete 注释增删改。 自定义UserDao, MyPlaneDao 和 ShoPlaneDao 接口用他们来定义要实现的语句。
( 6)定义一个 DBEngine 用来管理数据库,详细实现数据库的增删改查,用户不用在 Activity 类中调用 DAO 的接口,只需要在调用 DBEngine 书写的方法即可,减少Activity 类代码的冗余性。

三、服务器设计与实现

3.1 绑定端口

端口的实现和使用过程如下:
( 1) 创建服务器线程 ServerThread,在里面通过 new ServerSocket(9999)开放9999 号端口,同时开启动作捕捉线程 ActionThread。
( 2) 监听服务器端口。一旦数据被发送,它将被封装到一个套接字对象中。如果没有发送数据,它将处于线程阻塞状态,不会继续向下执行。 核心代码是 socket sc=ss. accept();
( 3) 一旦有数据发来,就执行动态代理线程ServerAgentThread, 负责和客户端交流数据。
( 4) 客户端为手机模拟器和服务端为真实手机,他们两个需要连在同一局域网下, 所以需要真实手机连接电脑的 WIFI。
( 5) 两个用户成功连接后,服务器端运行如图 3.16 所示
在这里插入图片描述

3.2 动作数据与提示信息同步

通过对动作数据的同步来更新队友屏幕上的动作,实现过程如下:
(1) 为套接字初始化输入输出流 new DataInputStream()和 new DataOutputStream()
(2) 在通过输入流 readUTF()接受数据时,服务器通过 startsWith(“<#CONNECT#>”)函数来检查是否带有连接字样开头的数据。
(3) 如果有则将客户端用户添加到 List 集合中,并返回个客户端连接成功字样即 writeUTF(“<#OK#>”),最多能连接两个客户,第一个为红飞机连接,第二个为黄飞机连接,两个飞机连接成功后提示客户端可以开始游戏的字样。 如果还有客户端想要继续连接则返回该客户端用户已经满的字样。
(4) 当两个飞机连接成功后,客户端将带有 KEY 字样的数据连同用飞机的偏移量一同发给服务器端,服务器代理器接收到带有 KEY 开头字样的数据以后将他通过 substring()和 split()进行分割,分别得到飞机的偏移量 X 和
Y 并存储到动作队列中。
(5) ActionThread 将新产生的红飞机或者黄飞机的 X 和 Y 得值从队列中获取出来并将动作移除队列,数据再在动作类 Action 中进行计算和范围限制,最后通过服务器代理的 broadcastState()方法发送给所有的客户端, 数据加上了 GAME_DATA 的前缀
(6) 客户端接收到数据后用 subString()和 Split()函数对数据进行拆分,并将飞机的 x 和 y 数据进行赋值,别通过绘制线程 DrawThread 进行实时更新。
(7) 注意在绘制飞机坐标数据时要加锁this.activity.gamedate.lock 防止服务器新来的游戏数据将之前的替换掉。还要注意关闭输入输出流和套接字。
核心代码:

synchronized(this.activity.gamedate.lock)
{
this.activity. gamedate.rx=trx;
this.activity. gamedate.ry=try;
this.activity. gamedate.gx=tgx;
this.activity. gamedate.gy=tgy;
}

四、其他功能实现

本应用中还有几个重要的技术需要深刻讨论,他们的实现也占据了项目开发的大部分时间,主要思考为数学逻辑和流程逻辑。

4.1 飞机碰撞的原理与实现

在解释碰撞原理之前我们要知道, event.getX()和 event.getRawX()的异样, event.getX()是触碰点相对于它所在组件坐标系的坐标; event.getRawX()是触碰点相对于屏幕默认坐标系的坐标。 横屏时, X 的值随着手机左侧到右侧逐渐变大, Y 的值随着手机从上到小变大,如图 3.25 所示
碰撞的双方为两个长方形图片,假设为 Rect1,中心点为(x1, y1),宽为w1,高为 h1 和 Rect2, 中心点为(x2, y2),宽为 w2,高为 h2,则如果同时满足如下四步则判断为碰撞:
x1+w1 >= x2, x2+w2 >= x1,y1+h1 >= y2,y2+h2 >= y1, 飞机碰撞关键代码如下:

(myPlaneBullet.getBullet_x()+myPlaneBullet.getBulletImgWidth()>=
enemyPlane.getPlane_x())
&&(enemyPlane.getPlane_x()+enemyPlane.getPlaneImgWidth()>=my
PlaneBullet.getBullet_x())
&&myPlaneBullet.getBullet_y()+myPlaneBullet.getBulletImgHeight()
>=enemyPlane.getPlane_y())
&&(enemyPlane.getPlane_y()+enemyPlane.getPlaneImgHeight()>=m
yPlaneBullet.getBullet_y()//子弹和飞机碰撞代码类似只是参数不同

原理图如图 3.17 所示。
在这里插入图片描述在这里插入图片描述

4.2 摇杆功能的实现

(1) 先找两个圆形摇杆图片一个大圆和一个小圆, 根据横屏时 x, y 坐标系设置摇杆图片的初始位置,小圆和大圆的圆心在同一位置,再设置动态圆的半径。 游戏中摇杆的形式主要有两种方式:固定式和跟随式。
本项目开发的摇杆为固定式,即内环圆心在外环圆内移动;外环位置固定不能移动。 更改方法为判断触碰点 P 是否在外环圆内, 若是则直接让内环到 P点, 若不是则计算新旧位置之间的向量[9]。
(2) 方向控制包括四个方向和八个方向。有一种常用的计算方法:计算内外环中心的角度,然后根据角度定义为四个方向: 上、下、左、右,以及八个方向:左上、左下、右上和右下。
角度用三角函数计算,过程如下:

  1. 得到两点 x 的距离。
  2. 得到两点 y 的距离。
  3. 算出斜边长度。
  4. 得到这个角度的余弦值。
  5. 通过反余弦函数得到弧度。
  6. 四方向和八方向判断。
    关键代码如下所示:
    //获取水平夹角弧度。
float lenA=x2-x1;
float lenB=y2-y1;
float lenC=(float) Math.sqrt(lenA*lenA+lenB*lenB);
float angle=(float)Math.acos(lenA/lenC);
angle=angle*(y2<y1?-1:1);
//获取长度
int result=(int)Math.sqrt(Math.pow(x-centerX, 2)+Math.pow(y-centerY, 2));

(3) 最后根据方向和飞机的移动速度, 修改飞机的坐标。

4.3 音乐服务的实现

(1) 游戏大厅背景音乐实现:选择非绑定式界面,让背景音乐在设置界面、我的飞机和商店界面仍能继续播放, 但是当进入游戏时将背景音乐关掉。播放游戏相关的音乐。 实现过程如下:

  1. 在服务中引用要播放的音乐,他们存放在 raw 文件夹下,用 int 型数组引用即可。
  2. 创建广播接收器 new MyReciver(),以便设置界面能够进行音乐的相关设置。
  3. 创建播放器 new MediPlayer(),通过音乐 ID 来播放音乐,关键代码如下:
mPlayer=MediaPlayer.create(this,music);//将音乐加入播放器
mPlayer.start();//播放音乐
mPlayer.setLooping(true);//允许循环播放
  1. 创建 IntentFilter 过滤器,使音乐服务器端的接收器只能接受客户端发来的 CTL_ACTION 消息,注册带有此筛选器的接收器。关键代码如下:
IntentFilter filter = new IntentFilter();
filter.addAction(Set_UpActivity.CTL_ACTION);
registerReceiver(serviceReceiver, filter);
  1. 为音乐播放器绑定监听器,监听是那首音乐和音乐的播放状态,并将它们通过广播发送个设置界面以便来查看音乐播放的状态。 关键代码如下:
Intent sendIntent = new Intent(Set_UpActivity.UPDATE_ACTION);
sendIntent.putExtra("update", status);
sendIntent.putExtra("current", current);
sendBroadcast(sendIntent);
  1. 通过服务中的线程来刷新设置界面中音乐的进度条。
    (2) 游戏音乐实现: 创建 MusicUtil 将要播放的音乐信息通过键值对的方式存放在 HashMap 中。 在游戏运行的线程 GameRunThread 中播放背景音乐,并且每当方式我方机和敌机,我方子弹和敌机碰撞时播放音乐特效。 核心代码如下:
MusicUtil.soundPool.play(MusicUtil.sounds.get(1),1,1,2,-1,1);

MusicUtil.sounds.get(1)为播放的音乐 ID 号,两个 1 为左右声道, 2 为优先级, -1为循环播放,最后一个 1 为正常倍速。

4.4 recycleview 适配器的实现

(1) recycleview 控件和适配器 Adapter 联合使用,能够将图片和文本大量的调入,使显示的方法更加容易。
(2) 导入 recycleview 依赖,如下:implementation ‘androidx.recyclerview:recyclerview:1.1.0’
(3) 相关适配器继承 RecyclerView.Adapter<MyPlaneAdapter.MyPlaneViewHolder>并重写它的方法:

  1. onCreateViewHolder()在这里面添加要显示的控件布局
  2. onBindViewHolder()在这里面这只每一个控件的详细内容
  3. getItemCount()获取控件数据的数量
    (4) 在适配器中书写内部类 MyPlaneViewHolder 继承 RecyclerView.ViewHolder 用来初始化控价,并为控件增加点击监听事件。
    (5) 在界面中调用适配器遵循四部原则:初始化适配器,初始化界面布局,设置界面布局和界面添加适配器。(顺序不可颠倒),关键代码如下:
Adapter adapter = new Adapter(相关参数);
GridLayoutManager gridLayoutManager = new GridLayoutManager(相关参数);
控件区域.setLayoutManager(gridLayoutManager)
控件区域.setAdapter(adapter);

(6) 当然数据需要通过 List 集合进行添加,先初始化 List:
List<对象> data = new ArrayList<>(); 再将每个对象的相关属性进行添加,最后通过 data.add(对象名)将每个对象添加到集合中去。

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

手可摘辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值