Android 游 戏 案 例 开 发 与 关 键 技 术 作 者 : 华 清 远 见 第 3 章 游 戏 中 的 多 线 程 本 章 简 介 在 游 戏 场 景 中, 我 们 可 以 看 到 各 个 游 戏 元 素 都 在 发 生 一 些 变 化, 比 如 主 角 的 移 动 背 景 的 变 化 道 具 的 运 动 等, 这 些 变 化 有 些 相 互 影 响, 有 些 则 不 然 在 捕 鱼 游 戏 中, 我 们 发 射 一 枚 炮 弹, 这 枚 炮 弹 会 沿 着 发 射 轨 迹 移 动, 鱼 群 也 会 以 不 同 的 方 向 自 由 地 移 动, 这 些 操 作 都 是 通 过 多 线 程 来 实 现 的 多 线 程 游 戏 模 型 的 算 法 思 想 是 : 将 各 游 戏 元 素 的 动 作 用 独 立 的 线 程 进 行 处 理, 特 定 功 能 模 块 用 指 定 的 线 程 进 行 处 理 我 们 在 程 序 中 实 现 的 都 是 模 拟 的 多 线 程, 用 以 处 理 不 同 的 事 物, 但 实 际 上 在 计 算 机 处 理 的 物 理 层, 仍 然 以 单 线 程 的 模 式 工 作, 或 者 同 时 运 行 计 算 机 内 核 数 量 的 线 程 Java 当 中 已 经 封 装 好 了 多 线 程 的 机 制, 即 Android 当 中 多 线 程 不 是 依 赖 于 虚 拟 机 的, 而 是 依 赖 于 底 层 Linux 的
3.1 多 线 程 的 使 用 3.1.1 游 戏 逻 辑 的 实 现 在 Android 游 戏 开 发 中, 逻 辑 部 分 通 常 由 线 程 实 现, 比 如 小 鱼 的 游 动 小 鱼 的 状 态 处 理 子 弹 的 路 径 碰 撞 检 测 等 这 里 我 们 以 小 鱼 游 动 为 例, 介 绍 游 戏 中 的 线 程 的 应 用 : 我 们 让 线 程 以 一 个 周 期 的 方 式 循 环 变 化 鱼 在 屏 幕 上 的 坐 标, 这 样, 每 次 绘 制 周 期 中, 绘 制 的 鱼 就 在 不 断 变 换, 看 起 来 就 像 是 鱼 儿 在 游 动 一 样 在 程 序 中 使 用 run() 方 法 来 控 制 每 条 鱼 的 坐 标, 代 码 清 单 3-1 为 FishRunThread.java 的 实 现 * 控 制 鱼 儿 游 动 的 线 程 * @author Xiloerfan * public class FishRunThread extends Thread{ private Fish fish; private boolean isrun; * 屏 幕 宽 度 private int screenwidth; public FishRunThread(Fish fish,int screewidth){ this.fish = fish; this.screenwidth = screewidth; public void stopfishrun(){ isrun = false; @Override public void run() { isrun = true; // TODO Auto-generated method stub int fishx = 0; while(isrun){ 3.1.2 创 建 多 个 线 程 * 累 加 X, 让 鱼 一 直 向 右 移 动 代 码 清 单 3-1 控 制 鱼 儿 游 动 的 线 程 fish.getpicmatrix().settranslate(fishx,0); fishx++; if(fishx>screenwidth){ * 减 去 宽 度 是 为 了 让 鱼 的 起 始 位 置 位 于 屏 幕 左 边 没 有 出 现 的 位 置 fishx = 0-fish.getCurrentPic().getWidth(); try { Thread.sleep(50); catch (Exception e) { // TODO: handle exception 掌 握 了 线 程 在 游 戏 中 实 现 功 能 的 方 法 后, 我 们 就 可 以 创 建 多 个 线 程 来 完 成 不 同 的 任 务 以 小 鱼 快 跑 2
为 例, 我 们 可 以 开 启 不 同 的 线 程 来 控 制 鱼 群 的 移 动 子 弹 的 发 射 等 3.2 多 线 程 的 注 意 事 项 3.2.1 同 步 问 题 在 以 上 代 码 中, 我 们 用 一 个 while 循 环 对 鱼 儿 的 位 置 进 行 处 理, 以 Boolean 型 变 量 isrun 来 作 为 判 断 的 条 件 isrun 为 true 代 表 游 戏 正 在 进 行, 当 游 戏 结 束 时 在 stopfishrun() 中 将 isrun 设 为 false, 退 出 循 环 这 样 做 对 一 个 单 独 的 线 程 没 有 影 响, 但 是 在 整 个 游 戏 当 中 若 存 在 两 个 或 两 个 以 上 的 线 程, 每 个 线 程 都 有 各 自 的 isrun 属 性, 当 其 中 一 个 线 程 退 出 时, 游 戏 就 应 该 结 束, 游 戏 中 所 有 的 线 程 都 应 该 结 束 但 是 这 时 只 有 引 起 游 戏 结 束 的 线 程 的 isrun 属 性 被 设 置 为 false, 而 其 他 线 程 仍 在 运 行, 这 样 将 会 导 致 游 戏 不 能 完 全 退 出 和 一 些 其 他 的 问 题 为 了 解 决 以 上 问 题, 我 们 用 一 个 Boolean 值 变 量 gameinfo 来 表 示 整 个 系 统 的 运 行 状 态, 并 将 其 存 储 于 一 个 公 开 类 GamingInfo.java 中, 使 用 get 和 set 方 法 来 获 取 和 修 改 它 的 值 在 程 序 运 行 时, 每 一 个 线 程 只 需 要 调 用 这 一 个 属 性, 就 可 以 获 知 游 戏 是 否 结 束 我 们 将 游 戏 中 其 他 的 公 共 属 性 都 存 储 于 GamingInfo.java 中, 用 相 应 的 get 和 set 方 法 来 使 用 代 码 清 单 3-2 为 GamingInfo.java 的 实 现, 代 码 清 单 3-3 为 FishRunTread.java 中 改 进 之 后 的 run() 方 法 // 游 戏 进 行 中 一 些 需 要 共 用 的 变 量 public class GamingInfo { private int screenwidth; private int screenheight; private static GamingInfo gameinfo; private boolean isgaming; private boolean ispause; 代 码 清 单 3-2 GamingInfo.java 的 实 现 // 单 例 模 式 需 要 // 是 否 处 于 游 戏 状 态 // 是 否 处 于 暂 停 状 态 private MainSurface surface; // 主 屏 幕 private Activity activity; private ArrayList<Fish> fish = new ArrayList<Fish>(); // 所 有 的 鱼 private ShoalManager shoalmanager; private SoundManager soundmanager; private float cannonlayoutx; private float cannonlayouty; private int score = 100; // 鱼 群 管 理 器 // 音 效 管 理 器 // 大 炮 旋 转 X 坐 标 // 大 炮 旋 转 Y 坐 标 // 当 前 的 分 数 值 public int getscore() { return score; public void setscore(int score) { this.score = score; * 清 除 GamingInfo 实 例 public static void cleargameinfo() { gameinfo = null; private GamingInfo() { public static GamingInfo getgaminginfo() { if (gameinfo == null) { 3
gameinfo = new GamingInfo(); return gameinfo; public boolean isgaming() { return isgaming; public void setgaming(boolean isgaming) { this.isgaming = isgaming; public ArrayList<Fish> getfish() { return fish; public void setfish(arraylist<fish> fish) { this.fish = fish; public MainSurface getsurface() { return surface; public void setsurface(mainsurface surface) { this.surface = surface; public ShoalManager getshoalmanager() { return shoalmanager; public void setshoalmanager(shoalmanager shoalmanager) { this.shoalmanager = shoalmanager; public int getscreenwidth() { return screenwidth; public void setscreenwidth(int screenwidth) { this.screenwidth = screenwidth; public int getscreenheight() { return screenheight; public void setscreenheight(int screenheight) { this.screenheight = screenheight; public Activity getactivity() { return activity; public void setactivity(activity activity) { this.activity = activity; public SoundManager getsoundmanager() { return soundmanager; public void setsoundmanager(soundmanager soundmanager) { 4
this.soundmanager = soundmanager; public float getcannonlayoutx() { return cannonlayoutx; public void setcannonlayoutx(float cannonlayoutx) { this.cannonlayoutx = cannonlayoutx; public float getcannonlayouty() { return cannonlayouty; public void setcannonlayouty(float cannonlayouty) { this.cannonlayouty = cannonlayouty; public boolean ispause() { return ispause; public void setpause(boolean ispause) { this.ispause = ispause; public void run() { 代 码 清 单 3-3 线 程 中 的 run() 方 法 int[][] path = PathManager.getDefaultPath(fish); while (GamingInfo.getGamingInfo().isGaming()) { while(isrun &&!GamingInfo.getGamingInfo().isPause()){ for (int[] pathmode : path) { if(fishisout!isrun){ // 如 果 路 径 为 旋 转 模 式 if (pathmode[0] == PathManager.PATH_MODE_ROTATE) { * 这 里 做 了 一 个 处 理, 就 是 分 析 鱼 头 朝 向 和 所 在 位 置 让 鱼 儿 进 行 不 同 的 旋 转 路 线 // 如 果 鱼 儿 处 于 第 1 或 第 2 象 限 if (fish.getfish_x() <= GamingInfo.getGamingInfo().getScreenWidth() / 2 && fish.getfish_y() <= GamingInfo.getGamingInfo().getScreenHeight() / 2 fish.getfish_x() > GamingInfo.getGamingInfo().getScreenWidth() / 2 && fish.getfish_y() <= GamingInfo.getGamingInfo().getScreenHeight() / 2) { // 如 果 鱼 头 是 朝 向 x 的 负 坐 标 方 向 if (fish.getcurrentrotate() >= 90 && fish.getcurrentrotate() <= 270 fish.getcurrentrotate() <= -90 && fish.getcurrentrotate() >= -270) { rotateleftfish(pathmode[1]); else { rotaterightfish(pathmode[1]); // 如 果 鱼 儿 处 于 第 3 或 第 4 象 限 else { // 如 果 鱼 头 是 朝 向 x 的 负 坐 标 方 向 if (fish.getcurrentrotate() >= 90 && fish.getcurrentrotate() <= 270 fish.getcurrentrotate() <= -90 && fish.getcurrentrotate() >= -270) { rotaterightfish(pathmode[1]); else { 5
// 如 果 路 径 为 直 行 模 式 rotateleftfish(pathmode[1]); else { gostraight(pathmode[1]); if(!fishisout){ // 重 新 获 取 新 路 径 path = PathManager.getDefaultPath(fish); else{ while(isrun && GamingInfo.getGamingInfo().isGaming()){ // 如 果 超 出 屏 幕, 走 一 个 直 线 gostraight(100); 专 业 始 于 专 注 卓 识 源 于 远 见 3.2.2 数 据 安 全 问 题 线 程 锁 游 戏 中 经 常 会 使 用 多 线 程 来 访 问 同 一 个 数 据, 这 样 可 能 会 带 来 数 据 的 不 安 全 问 题, 比 如 两 个 线 程 T1 和 T2 都 访 问 同 一 数 据 D, 当 T1 开 始 改 变 D 的 值 时, 理 论 上 D 应 该 是 新 的 值, 这 时 若 T2 恰 好 也 要 访 问 D, 而 D 并 没 有 被 完 全 改 变, 那 么 T2 可 能 会 得 到 D 原 来 的 值, 即 得 到 错 误 的 信 息 为 了 避 免 这 种 情 况 的 发 生, 我 们 使 用 线 程 锁 来 解 决, 即 当 D 的 值 正 在 改 变 时, 给 D 加 一 个 线 程 锁, 使 任 何 其 他 线 程 都 无 法 访 问, 直 到 完 成 了 D 值 的 改 变 我 们 在 更 新 图 层 的 时 候 用 到 了 线 程 锁, 代 码 清 单 3-4 显 示 了 更 新 图 层 的 过 程 代 码 清 单 3-4 更 新 图 层 * 更 新 图 层, 这 里 分 为 三 种 操 作 方 式, 分 别 是 更 新 临 时 图 层 中 的 内 容 到 绘 制 图 层 中, 删 除 绘 制 图 层 中 的 元 素, 添 加 绘 制 图 层 中 的 元 素 * 这 里 加 了 个 线 程 锁, 保 证 多 线 程 下 操 作 图 层 的 安 全 性 * @param mode 绘 制 图 层 的 操 作 类 型, 对 应 当 前 类 的 CHANGE_MODE 常 量 * @param layerid 操 作 的 图 层 ID * @param draw 操 作 的 图 层 元 素 private synchronized void updatepiclayer(int mode,int layerid,drawable draw){ switch(mode){ // 将 临 时 图 层 中 的 内 容 更 新 至 绘 制 图 层 中 case CHANGE_MODE_UPDATE: // 如 果 有 修 改 if(changelayer){ // 向 图 层 添 加 新 的 元 素 for(integer id:addpiclayer.keyset()){ for(drawable d:addpiclayer.get(id)){ // 如 果 要 添 加 的 元 素 所 处 图 层 不 存 在, 则 创 建 这 个 图 层, 并 更 新 图 层 ID 数 组 if(this.piclayer.get(id)==null){ this.piclayer.put(id, new ArrayList<Drawable>()); updatelayerids(id); this.piclayer.get(id).add(d); addpiclayer.clear(); // 删 除 图 层 中 的 元 素 for(integer id:removepiclayer.keyset()){ for(drawable d:removepiclayer.get(id)){ try { this.piclayer.get(id).remove(d); catch (Exception e) { System.out.println(" 图 层 内 容 不 存 在 :"+id); 6
removepiclayer.clear(); changelayer = false; * 无 论 是 向 绘 图 图 层 中 添 加 还 是 删 除 元 素, 都 不 是 直 接 操 作 绘 制 图 层, 都 是 存 放 在 对 应 的 临 时 图 层 中, 等 待 绘 制 方 法 绘 制 周 期 中 将 变 化 的 内 容 更 新 到 绘 制 图 层 中 * 保 证 多 线 程 操 作 情 况 下 的 安 全 性 // 添 加 一 个 元 素 case CHANGE_MODE_ADD: ArrayList<Drawable> al = addpiclayer.get(layerid); if(al==null){ al = new ArrayList<Drawable>(); addpiclayer.put(layerid, al); al.add(draw); changelayer = true; // 删 除 一 个 元 素 case CHANGE_MODE_REMOVE: ArrayList<Drawable> al1 = removepiclayer.get(layerid); if(al1==null){ al1 = new ArrayList<Drawable>(); removepiclayer.put(layerid, al1); al1.add(draw); changelayer = true; 既 然 线 程 这 么 方 便, 那 可 以 开 无 数 个 线 程 吗? 孩 子 啊, 其 实 不 可 以 每 个 线 程 都 会 占 用 程 序 的 资 源, 使 程 序 花 费 一 定 的 时 间 处 理 资 源 如 果 线 程 过 多, 会 降 低 程 序 的 运 行 速 度, 游 戏 就 会 很 卡, 很 可 能 没 办 法 玩 下 去 3.3 本 章 小 结 本 章 主 要 介 绍 了 在 游 戏 中 使 用 线 程 处 理 逻 辑 和 使 用 多 线 程 的 方 法, 并 提 到 了 使 用 多 线 程 可 能 会 遇 到 的 问 题 7
及 常 用 解 决 方 案 专 业 始 于 专 注 卓 识 源 于 远 见 联 系 方 式 集 团 官 网 :www.hqyj.com 嵌 入 式 学 院 :www.embedu.org 移 动 互 联 网 学 院 :www.3g-edu.org 企 业 学 院 :www.farsight.com.cn 物 联 网 学 院 :www.topsight.cn 研 发 中 心 :dev.hqyj.com 集 团 总 部 地 址 : 北 京 市 海 淀 区 西 三 旗 悦 秀 路 北 京 明 园 大 学 校 内 华 清 远 见 教 育 集 团 北 京 地 址 : 北 京 市 海 淀 区 西 三 旗 悦 秀 路 北 京 明 园 大 学 校 区, 电 话 :010-82600386/5 上 海 地 址 : 上 海 市 徐 汇 区 漕 溪 路 250 号 银 海 大 厦 11 层 B 区, 电 话 :021-54485127 深 圳 地 址 : 深 圳 市 龙 华 新 区 人 民 北 路 美 丽 AAA 大 厦 15 层, 电 话 :0755-25590506 成 都 地 址 : 成 都 市 武 侯 区 科 华 北 路 99 号 科 华 大 厦 6 层, 电 话 :028-85405115 南 京 地 址 : 南 京 市 白 下 区 汉 中 路 185 号 鸿 运 大 厦 10 层, 电 话 :025-86551900 武 汉 地 址 : 武 汉 市 工 程 大 学 卓 刀 泉 校 区 科 技 孵 化 器 大 楼 8 层, 电 话 :027-87804688 西 安 地 址 : 西 安 市 高 新 区 高 新 一 路 12 号 创 业 大 厦 D3 楼 5 层, 电 话 :029-68785218 广 州 地 址 : 广 州 市 天 河 区 中 山 大 道 268 号 天 河 广 场 3 层, 电 话 :020-28916067 8