代码下载地址:http://download.csdn.net/source/1047937

SLG或者说战棋游戏,在大多数英文站点是归类到Simulation Game的(包括模拟城市之类的纯SIM),并没有进行SRPG(Strategies Role Play Games)、RTS(Real-Time Strategy Game)乃至RSLG(Role play Simulation Game)种种的细分。归结原因,想必还是因为近似因素太多,在大多数时候已经难以区分其本来面貌,只能一概而论,所以本文也可以理解为SRPG或 RSLG开发的入门示例。

前言:

这是一篇孤立的博文,精简了示例代码效果及行数,仅保留最基础功能,与以前写过的[Java版SLG游戏开发入门]没有直接联系,但可以互相参看。

关于AI:

AI(Artificial Intelligence),即人工智能,有时也称作机器智能或人工脑,是指那些由人类制造出来的系统,在面对具体事务时,所表现出的类人反应。通常情况下人工智能多指以人类思维模式为准绳,通过计算机模拟实现的智能。

人工智能的定义可以分为两部分,即“人工”和“智能”。“人工”比较好理解,争议性也不大。有时我们会要考虑什么是人力所能及制造的,或著人自身的智能程度有没有高到可以创造人工智能的地步,等等。但总的来说,“人工系统”就是通常意义下的人工系统。

举凡涉及到什么是“智能”的话题,就问题多多……人唯一了解的智能是人本身的智能,这是普遍认同的观点。但我们对自身智能的理解也都非常有限,对构成人的智能的必要元素也知之甚少,所以很难真正定义什么是“人工”制造的“智能”。

据此,我同样很怕写涉及到AI题材的博文,首先AI处理本就是个有待研究的领域,因为甚至连[智能]究竟是什么都是个很难解释的问题,这其中还涉及到其它诸如意识(consciousness)、自我(self)、心灵(mind)(包括无意识的精神(unconscious_mind)等等,存在相当大程度的争议。

而评判AI的标准也不尽相同,抛开还很遥远的人工生命(“强人工智能”或“弱人工智能”),似乎要成为标准的“机器人三定律”,图灵测试等等不说,单从游戏AI引擎的设计角度讲,这已经是个很严肃的话题,并非一两行代码就能构建完成的,负责任的说,如果要严谨的写出一个中等规模战棋游戏的AI处理代码,并配合图文解释且标注上参考文献,加上关键字摘要等等,差不多就是篇硕士论文|||,总之水很深。考虑到篇幅及鄙人水平因素,故此文中并没有深入探究,仅仅给出一个“入门示例”,供看客参考而已。

正文:

事实上我们之所以喜欢游戏,很大程度上是基于“与天斗,与地斗,与人斗,其乐无穷”的理由,相信极少有玩家会喜欢战场上的敌人永远一动不动任你蹂躏,更不会有人喜欢仅仅出现You win again字样的游戏。应该说,游戏中的AI很大程度上讲是体现在电脑与玩家的对抗中,一款好的游戏AI应该能足够刺激玩家,“蹂躏”玩家,吸引玩家参与对抗。

不妨这样讲,制作游戏的目的相较于体现玩家的“聪明”,倒不如说是更希望看见他们的“愚蠢”,看见他们被游戏玩弄得惊慌失措,叫苦连天,还乐此不疲的“憨佬”形象,这才是我们作为游戏开发者的最大快乐(^^)~

根据具体事件的不同,游戏AI可具体体现在以下两个方面:

一、单元活动AI(Unit Behavioral AI)

游戏AI并不总是标准含义上的AI。而单元(也可理解为角色或者基本对象,以下同)游戏AI正是为设计出具有提供某种挑战或某种真实体现的生命特征的一次真正的尝试。

譬如在玩家与游戏的互动中,只站在一处、从不移动的警卫会显得非常不真实。不过,如果你创建一个例行程序(routine),使他不时的朝四周张望,或变换他的姿势,他会看起来更具活力。通过创建一个在预设的路径上行走的警卫偶然停在站岗的警卫前,并好像与他谈话这样的情景,真实的体现能被极大地提高。单元活动AI,正是出于这种目的制作的“拟人性”而非“拟人”AI。

在单元AI中,动作模式可分为被动式(Passive)与自发式(Spontaneous)两大类。

1、被动式:现实生活中,如果有人打了你的左脸,要不然就伸出右脸让他再打,要不然就伸出右手还他一巴掌,总之,你会有相应的“反应”。而被动式AI,正是这种情况的体现。

在被动模式下,单元(角色)随时会对自身环境中的变化做出响应。如果一个敌人发现了你,开始向你跑来,并朝你射击,那么他们已经做出了看到你的反应。

2、自发式:在自发模式下,单元做出行动时并不依赖于自身环境中的任何变化。一个单元决定从其所站立的岗哨移向基地周围的某个游动岗哨,则这个单元已经做出了一次自发性的行动。

通过在你的游戏中加入不同的单元活动元素,就能够制造出单元的“聪明”假象,令玩家产生对手如同真人的错觉。

二、单元行动AI(Unit Actions AI)

好比人类的智商是体现在行为及对世情的准确判断上,真正让一个游戏单元看起来聪明或者愚蠢的,同样是他们的行动。

简单的说,如果游戏单元依照玩家认为可行的方式移动,或者在玩家认知范围合理的情景下做如闪避这样的动作,那么单元看起来会很聪明,相反则会给人愚蠢的印象。但是,实际开发中这种现象都并非真实存在,而是看起来聪明或愚蠢的假象,因为程序仅仅与玩家面对的基本情景相关联,而并非游戏中角色真的聪明抑或愚蠢。

如果你处理恰当,且这一应用包含的范围广泛,你的玩家就会相信你的单元足够“聪明”。为了实现这一目的,你需要把自己放在你所构建单元的位置上思考,如果把你丢到游戏中,在他们的情景下你会怎么做?你将怎样回应各种各样的攻击或遭遇敌人?如果什么事都根本没发生,你又将会做些什么?

如果你回答了这些问题,并针对你的单元将遇到的每种情景正确的实施了它们,你拥有“看似聪明”单元的机会就会最大化,这也是创建一个优秀的、稳健的游戏AI的第一步。

谈过了单元行为,我们再来说说单元运行中的事件分类。

根据处理事件采取的不同技术,游戏AI又可分为确定性(deterministic)AI与非确定性(Non-deterministic)AI两大类别:

1、确定性AI:

确定性AI的单元(角色)行为或者说表现是特定的,可预测,没有任何不确定因素。其具体实现如同我在博文[Java伪寻径追踪实现]中展示的单元追逐演算,一个非玩家单元紧随玩家单元X,Y坐标前进,直至与玩家单元或目标点重叠为止。

2、非确定性AI:

与确定性AI相反,非确定性AI在行为模式上存在着很大程度上的不可预测性,理论上讲甚至能够令单元(角色)做出很多超出程序员构想的突现行为。简单实现可见随本博文发布的程序示例(单元随机动作),但其复杂实现则需要应用到神经网路、贝斯叶概率模式、乃至基因演算法等相关知识支撑。故此鄙人对严谨意义上的非确定性AI也不敢置喙太多,深入研究有待看客自行探索。

3、[隐藏类别] AI处理结果欺诈(流氓手段、、赖招,随便叫(-_-|||)):

这种方式事实上是程序员心智肚明,却又讳莫如深的一种编程技巧,我读书时老师戏称其为“流氓手段”。还记得在当时课堂上,老师曾举过这样一个例子,让我们写出一段能够得到1-100相累加结果的最简代码,同学们发言很踊跃,但是却没人有正确答案。而当我们质疑老师的评判标准时,老师却给出了绝对最简的答案——直接显示5050。

应该说,在处理绝对可知结果时,这种方式确实非常有效,对于频繁运算来讲更能体现其价值所在;而对于游戏中某些运算复杂,但结果却单一的事件,确实可以采用“流氓手段”进行编程,即可提高效率,又减少了代码量,但却决不能轻易被用户知道,尤其是在网游的转、精炼等结果中……

总结:

就我目前所知的游戏AI实现中,确定性AI可谓绝对主流,因为它的结果固定有穷,所以相对于非确定AI占用程序资源更小,效率更高,实现也更简单。但有利必有弊,对一个聪明的玩家而言,找出一个确定性AI的规律是再简单不过的事情,有限的行动模式,也必然决定游戏可玩性同样有限。

而非确定性AI,则毫无疑问是块雷区,无论对我这种业余玩票性质抑或专业游戏开发者尽都如此,它已经无限延伸入“人工生命”这块“神之领域”,并非短时间就能够学习甚至使用的技术。但如果能在程序中成功利用,则无疑会极大增强游戏可玩性。

至于我提到的“流氓手段”,则只能意会,不可言传,大家心照不宣。

总体来讲,游戏AI无论是确定性或非确定性,单元(角色)都难免如同巴浦洛夫狗流哈喇子中的dog那样,仅仅会对特定事件做出“条件反射”,依据制作者设定好的行为模式而并非角色的自主思维运作,行为可能性是“有穷”的,并没有如人类般拥有“无限可能性”,故此可以看作一种“伪智能”,而非严格意义上的“人工智能”。当然,我相信随着技术的发展,这种“伪智能”技术最终将进化为真正意义上的“人工智能”。

对于具体处理流程,则可作如下分类:

1、有限状态机(Finite State Machin,FSM):

最廉价、同时也是最实用的技术。在游戏实现(非游戏实现有出入)中的基本运作方式是采用穷举方式,罗列出单元所有可能的动作或状态,再利用switch、if等方式判定各种事件关系及满足条件,据此变更单元的动作或状态,由于我们所能做的仅是编辑从一状态到另一状态的转换,完成这系列一行为的算法,就可归属于分层有限状态机。

2、模糊状态机(Fuzzy State Machine,FuSM):

当利用随机数等方式触发模糊逻辑(fuzzy logic)时,会令单元的动作较难预计,产生大量新的分支判断,这时处理多个有限状态机情况的技术实现,就是模糊状态机,它以看“不精准”的响应来进行不确定性结果的处理。

3、分层有限状态机(Hierarchical Finite State Machines,HFSM)及扩展分层有限状态机(Extended Hierarchical Finite State Machines,EHFSM):

这两项技术可视同有限状态机与模糊状态机的融合体,他们尝试以一种树状结构分别处理有限及模糊状态,是一系列由同一个支点扩展开的行为模式树,不同的是扩展分层有限状态机有更为严密的控制流及数据流,当然代价是对于游戏系统的资源损耗也更多。

以上是AI引擎开发中常用的一些基础概念,如果想深入了解相关细节,还需看客自行深入研究。

关于单元(角色)寻径:

如果单元拥有AI,那么他理所应当的能够自主行动。但是,我们都知道游戏中角色是不存在或者说很难实现真正意义上AI的,所以与AI处理同样,寻径同样是我们这些程序员的一种“欺诈手段”,用以“蒙蔽”用户,让他们产生单元拥有自主思维的错觉。

关于常见的几种寻径方式,可见参本人博文[Java中的A*(A star)寻径实现]以及[Java伪寻径追踪实现],不再赘述。

简单的说,平面图是由x、y两点构成的,而所谓寻径就是在网格化的地图上连接出点到点间的路线交集;如果我们以二维数组mapList表示地图数据,moveList表示地图上可移动点的话,那么复合mapList地图数据及moveList上可移动点所构成的交集,就是寻径后得到的单元行走路线,即寻径结果。

相较于AI部分,寻径可以看作AI实现中的一项分支技术,个人认为没有太过深入探究理论的必要,唯一需要关心的,仅在于多对象寻径时的效率或准确性取舍问题,同样请参考相关技术文献,否则本文随时超出文章最大字数……

具体到游戏实现流程:

SLG离不开战场及角色与各种事件判断,而具体到其构建过程,大多遵循如下顺序:

1、地图(背景战场图)构建

2、获得对应地图基本单元的对象集合(2D游戏中多为二维数组)

3、获得可移动点的对象集合

4、创建地图上角色(针对于地图基本单元放置)

5、激活键盘或者鼠标事件(处理如光标移动等)

6、根据选择的不同在地图单元上绘制相光事件触发物(菜单等),以供事件触发

7、当选择事件时,事件处理开始,各单元(角色)根据预先设定响应事件反馈给玩家(比如移动、攻击等事件)

8、当我方全部行动结束或者选择结束后,敌军开始由AI自行处理事件

9、判定是否满足战斗结束条件

10、如果未满足步骤9,则循环回步骤1,同时回合数+1,游戏继续

在这一过程中,还可以加入如兵种、物品、特殊人物加成等影响性数据,但基本流程不受影响。

具体到演示代码:

本博文附带的演示代码有核心基本类如下,具体请参见代码注释:

Role.java(角色处理,包含敌我双方)

package org.loon.simple.slg.ai; import java.awt.Image; /** * Copyright 2008 - 2009 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * @project loonframework * @author chenpeng * @email:ceponline@yahoo.com.cn * @version 0.1 */ public class Role { //名称 String name; //分队(0:我军 1:敌军) int team; //hp int hp; //角色图像 Image image; //移动力 int move; //行动状态(0:未行动 1:已行动) int action; //x坐标 int x; //y坐标 int y; //是否已进行攻击 boolean isAttack = false; /** * 设定角色参数 * * @param name * @param team * @param image * @param move * @param x * @param y */ public Role(String name, int team, Image image, int move, int x, int y) { this.name = name; this.team = team; this.hp = 10; this.image = image; this.move = move; this.x = x; this.y = y; } public int getAction() { return action; } public void setAction(int action) { this.action = action; } public int getHp() { return hp; } public void setHp(int hp) { this.hp = hp; } public Image getImage() { return image; } public void setImage(Image image) { this.image = image; } public int getMove() { return move; } public void setMove(int move) { this.move = move; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getTeam() { return team; } public void setTeam(int team) { this.team = team; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public boolean isAttack() { return isAttack; } public void setAttack(boolean isAttack) { this.isAttack = isAttack; } }

Map.java(地图处理)

package org.loon.simple.slg.ai; import java.awt.Graphics; import java.awt.Image; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; /** * Copyright 2008 - 2009 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * @project loonframework * @author chenpeng * @email:ceponline@yahoo.com.cn * @version 0.1 */ public class Map { private int[][] mapArray = null; private int size = 0; private int maxX = 0; private int maxY = 0; private int tile = 32; private Image[] mapImages; /** * 加载指定地图文件为list * * @param fileName * @return * @throws IOException */ private static List loadList(final String fileName) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader( Utility.getResource(fileName))); List records = new ArrayList(10); String result = null; try { // 分解地图设定点 while ((result = reader.readLine()) != null) { char[] charArray = result.toCharArray(); int size = charArray.length; int[] intArray = new int[size]; for (int i = 0; i < size; i++) { intArray[i] = Character.getNumericValue(charArray[i]); } records.add(intArray); } } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { } } } return records; } /** * 加载地图文件为整型二维数组 * * @param fileName * @return * @throws IOException */ public static int[][] loadArray(final String fileName) throws IOException { // 取得地图二维数组(此时row,col颠倒) List list = loadList(fileName); int row = list.size(); int[][] mapArray = new int[row][]; for (int i = 0; i < row; i++) { mapArray[i] = (int[]) list.get(i); } int col = (((int[]) mapArray[row > 0 ? row - 1 : 0]).length); // 颠倒二维数组row,col int[][] result = new int[col][row]; for (int j = 0; j < col; j++) { for (int i = 0; i < row; i++) { result[i][j] = mapArray[j][i]; } } return result; } /** * 构造地图 * * @param size * @param fileName */ public Map(final String fileName, final int tile) { try { this.mapArray = Map.loadArray(fileName); } catch (IOException e) { throw new RuntimeException(e); } this.size = this.mapArray.length; this.tile = tile; this.maxX = this.size; this.maxY = (((int[]) this.mapArray[this.maxX > 0 ? this.maxX - 1 : 0]).length); this.mapImages = Utility.getSplitImages("image/map.png", tile, tile); } /** * 获得地图图像 * * @return */ public Image getMapImage() { Image image = Utility.createImage(maxX * tile, maxY * tile, true); Graphics graphics = image.getGraphics(); for (int y = 0; y <= maxY - 1; y++) { for (int x = 0; x <= maxX - 1; x++) { int type = getMapType(x, y); graphics.drawImage(mapImages[type], x * tile, y * tile, null); } } graphics.dispose(); return image; } public int[][] getMaps() { return mapArray; } public int getMaxX() { return maxX; } public int getMaxY() { return maxY; } /** * 获得地图指定坐标点对象 * * @param x * @param y * @return */ public int getMapType(int x, int y) { /*if (x < 0) { x = 0; } if (y < 0) { y = 0; } if (x > maxX - 1) { x = maxX - 1; } if (y > maxY - 1) { y = maxY - 1; }*/ return mapArray[x][y]; } /** * 返回指定坐标地形 * * @param x * @param y * @return */ public int getMapCost(int x, int y) { int type = getMapType(x, y); switch (type) { case 0: type = 1; // 草 break; case 1: type = 2; // 树 break; case 2: type = 3; // 山地 break; case 3: type = -1; // 湖泽(不能进入) break; } return type; } }

GameCanvas.java(战场绘制及各种事件处理)

package org.loon.simple.slg.ai; import java.awt.Canvas; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Random; /** * Copyright 2008 - 2009 * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * @project loonframework * @author chenpeng * @email:ceponline@yahoo.com.cn * @version 0.1 */ public class GameCanvas extends Canvas implements Runnable, KeyListener { /** * */ private static final long serialVersionUID = 1L; // 地图 private Map map = null; // 菜单 private Menu menu = null; // 背景窗体 private Image screen; // 地图图片 private Image mapImage; private Graphics2D graphics; private String state; private int lastX; private int lastY; private int curX; private int curY; private int turn = 1; private int actionUnit = -1; private int moveCount = 0; private int[][] moveList; private int[][] movingList; private int[][] attackList; private int maxX; private int maxY; private List unitList = Collections.synchronizedList(new ArrayList(10)); // 战斗个体图 private Image[] unitImages = Utility.getSplitImages("image/unit.png", tile, tile); private Image[] iconImages = Utility.getSplitImages("image/icon.png", tile, tile); private Image[] listImages = Utility.getSplitImages("image/list.png", tile, tile); private Thread gameLoop; private int eventCode = -1; final static int tile = 32; public GameCanvas() { actionUnit = -1; state = "战斗开始"; turn = 1; this.setBackground(Color.BLACK); this.map = new Map("map.txt", tile); // 地图 this.mapImage = this.map.getMapImage(); this.maxX = map.getMaxX(); this.maxY = map.getMaxY(); this.moveList = new int[maxX][maxY]; this.movingList = new int[maxX][maxY]; this.attackList = new int[maxX][maxY]; int width = maxX * tile; int height = maxY * tile; // 菜单 this.menu = new Menu(maxX - 1); // 创建角色:name=空罐少女,team=0(我军),imageindex=3,x=7,y=1,以下雷同 createRole("空罐少女", 0, 0, 3, 7, 1); createRole("猫猫1", 0, 1, 6, 1, 2); createRole("猫猫2", 0, 0, 3, 2, 6); // 创建角色:name=躲猫兵团1,team=1(敌军),imageindex=6,x=4,y=5,以下雷同 createRole("躲猫兵团1", 1, 2, 4, 4, 5); createRole("躲猫兵团2", 1, 2, 4, 8, 5); createRole("躲猫兵团3", 1, 2, 4, 5, 7); createRole("躲猫兵团4", 1, 2, 4, 7, 2); this.screen = Utility.createImage(width, height, true); this.graphics = (Graphics2D) screen.getGraphics(); // 初始化 this.initRange(); // 绘制图像 this.drawBattle(); this.setPreferredSize(new Dimension(width - 2, height - 2)); this.setFocusable(true); this.addKeyListener(this); // 开始构建游戏 this.mainLoop(); } public void mainLoop() { gameLoop = new Thread(this); gameLoop.start(); } public void run() { for (;;) { long start = System.currentTimeMillis(); long end = System.currentTimeMillis(); long time = end - start; long sleepTime = 20L - time; if (sleepTime < 0L) { sleepTime = 0L; } this.eventClick(); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { } } } /** * 事件触发 * */ public synchronized void eventClick() { switch (eventCode) { // 按下Enter,开始触发游戏事件 case KeyEvent.VK_ENTER: int index = 0; // 当游戏状态为[状态显示]下 if (state.equalsIgnoreCase("状态显示")) { // 光标指向我方未行动角色 index = getRoleIdx(0, curX, curY); if ((index > -1) && (getRole(index).action == 0)) { state = "角色移动"; actionUnit = getRoleIdx(0, curX, curY); // 绘制移动范围 setMoveRange(); movingList[curX][curY] = moveCount; drawBattle(); // 光标指向敌方未行动角色 } else if (getRoleIdx(1, curX, curY) > -1) { state = "移动范围"; actionUnit = getRoleIdx(1, curX, curY); setMoveRange(); drawBattle(); // 查看角报 } else { state = "情报查看"; openMenu(0); drawBattle(); } } // 选择移动 else if (state.equalsIgnoreCase("角色移动")) { // 无法移动的区域 if (moveList[curX][curY] < 0) { return; } // 监测移动地点 if ((getRoleIdx(0, curX, curY) == -1) || (moveList[curX][curY] == 0)) { lastX = getRole(actionUnit).x; lastY = getRole(actionUnit).y; moveRole(); state = "行动菜单"; // 绘制攻击范围 setAttackRange(true); // 判定菜单项 if (isAttackCheck()) { openMenu(2); } else { openMenu(1); } drawBattle(); } } // 当角色移动后 else if (state.equalsIgnoreCase("行动菜单")) { if (menu.getMenuItem(menu.cur).equalsIgnoreCase("攻击")) { state = "进行攻击"; closeMenu(); drawBattle(); } else if (menu.getMenuItem(menu.cur).equalsIgnoreCase("待机")) { state = "状态显示"; closeMenu(); getRole(actionUnit).action = 1; actionUnit = -1; initRange(); drawBattle(); } } // 攻击时 else if (state.equalsIgnoreCase("进行攻击")) { // 无法攻击 if (attackList[curX][curY] < 2) { return; } // 当指定地点敌方存在时 if ((index = getRoleIdx(1, curX, curY)) > -1) { // 删除List中敌方角色(此处可设定减血规范) unitList.remove(index); state = "状态显示"; // 改变行动状态 getRole(actionUnit).action = 1; actionUnit = -1; initRange(); drawBattle(); } } // 查看角色移动范围 else if (state.equalsIgnoreCase("移动范围")) { state = "状态显示"; Role role = getRole(actionUnit); curX = role.x; curY = role.y; actionUnit = -1; initRange(); drawBattle(); } // 查看角报 else if (state.equalsIgnoreCase("情报查看")) { // 本回合战斗结束 if (menu.getMenuItem(menu.cur).equalsIgnoreCase("结束")) { closeMenu(); curX = 0; curY = 0; setBeforeAction(); state = "战斗结束"; drawBattle(); } } // 我军开始行动 else if (state.equalsIgnoreCase("战斗开始")) { state = "状态显示"; drawBattle(); } // 敌军开始行动 else if (state.equalsIgnoreCase("战斗结束")) { state = "敌方行动"; enemyAction(); setBeforeAction(); turn = turn + 1; state = "战斗开始"; drawBattle(); } break; // 按下ESC,取消已做选择 case KeyEvent.VK_ESCAPE: if (state.equalsIgnoreCase("角色移动")) // 移动 { state = "状态显示"; Role role = (Role) unitList.get(actionUnit); curX = role.x; curY = role.y; actionUnit = -1; initRange(); drawBattle(); } else if (state.equalsIgnoreCase("行动菜单")) // 移动后 { state = "角色移动"; closeMenu(); setAttackRange(false); // 不显示攻击范围 Role role = (Role) unitList.get(actionUnit); role.x = lastX; role.y = lastY; drawBattle(); } else if (state.equalsIgnoreCase("进行攻击")) // 攻击状态 { state = "行动菜单"; Role role = (Role) unitList.get(actionUnit); curX = role.x; curY = role.y; openMenu(menu.menuType); drawBattle(); } else if (state.equalsIgnoreCase("移动范围")) { // 移动范围 state = "状态显示"; Role role = (Role) unitList.get(actionUnit); curX = role.x; curY = role.y; actionUnit = -1; initRange(); drawBattle(); } else if (state.equalsIgnoreCase("情报查看")) // 角报 { state = "状态显示"; closeMenu(); drawBattle(); } else if (state.equalsIgnoreCase("战斗开始")) // 我军行动 { state = "状态显示"; drawBattle(); } else if (state.equalsIgnoreCase("战斗结束")) // 敌军行动 { state = "敌方行动"; enemyAction(); setBeforeAction(); turn = turn + 1; state = "战斗开始"; drawBattle(); } break; } if (eventCode > -1) { eventCode = -1; } } /** * 初始化各项范围参数 * */ public synchronized void initRange() { for (int y = 0; y <= maxY - 1; y++) { for (int x = 0; x <= maxX - 1; x++) { moveCount = 0; moveList[x][y] = -1; movingList[x][y] = -1; attackList[x][y] = 0; } } } /** * 获得移动到指定地点所需步数 * * @param x * @param y * @return */ public synchronized int getMoveCount(int x, int y) { if ((x < 0) || (x > maxX - 1) || (y < 0) || (y > maxY - 1)) { // 无法移动返回-1 return -1; } return moveList[x][y]; } /** * 设定移动步数 * * @param x * @param y * @param count */ public synchronized void setMoveCount(int x, int y, int count) { Role role = getRole(actionUnit); // 当为我军时 if (role.team == 0) { if (getRoleIdx(1, x, y) > -1) { return; } } else { if (getRoleIdx(0, x, y) > -1) { return; } } int cost = map.getMapCost(x, y); // 指定位置无法进入 if (cost < 0) { return; } count = count + cost; // 移动步数超过移动能力 if (count > role.move) { return; } // 获得移动所需步数 if ((moveList[x][y] == -1) || (count < moveList[x][y])) { moveList[x][y] = count; } } /** * 设定攻击范围 * * @param isAttack */ public synchronized void setAttackRange(final boolean isAttack) { try { int x, y, point; if (isAttack == true) { point = 2; } else { point = 1; } Role role = getRole(actionUnit); x = role.x; y = role.y; // 判定攻击点 if (x > 0) { attackList[x - 1][y] = point; } if (y > 0) { attackList[x][y - 1] = point; } if (x < maxX - 1) { attackList[x + 1][y] = point; } if (y < maxY - 1) { attackList[x][y + 1] = point; } } catch (Exception e) { } } /** * 判断是否能做出攻击 * * @return */ public synchronized boolean isAttackCheck() { for (int i = 0; i < unitList.size(); i++) { Role role = getRole(i); if (role.team != 1) { continue; } if (attackList[role.x][role.y] == 2) { return true; } } return false; } /** * 设定菜单 * * @param menuType */ public synchronized void openMenu(int menuType) { menu.visible = true; menu.setMenuType(menuType); menu.cur = 0; } /** * 关闭菜单 * */ public synchronized void closeMenu() { menu.visible = false; } /** * 设定所有角色参与行动 * */ public synchronized void setBeforeAction() { for (Iterator it = unitList.iterator(); it.hasNext();) { Role role = (Role) it.next(); role.setAction(0); } } /** * 返回指定索引下角色 * * @param index * @return */ public synchronized Role getRole(final int index) { if (unitList != null && index > -1) { return (Role) unitList.get(index); } return null; } /** * 设定移动路线 * */ public synchronized void setMoveCourse() { if (moveList[curX][curY] == -1) { return; } if (movingList[curX][curY] == moveCount) { return; } // 选择可行的最短路径 if ((movingList[redressX(curX - 1)][curY] != moveCount) && (movingList[curX][redressY(curY - 1)] != moveCount) && (movingList[redressX(curX + 1)][curY] != moveCount) && (movingList[curX][redressY(curY + 1)] != moveCount) || (moveCount + map.getMapCost(curX, curY) > getRole(actionUnit).move)) { for (int j = 0; j <= maxY - 1; j++) { for (int i = 0; i <= maxX - 1; i++) { movingList[i][j] = -1; } } int x = curX; int y = curY; moveCount = moveList[curX][curY]; movingList[x][y] = moveCount; // 获得移动路径 for (int i = moveCount; i > 0; i--) { switch (setMoveCouse(x, y)) { case 0: x = x - 1; break; case 1: y = y - 1; break; case 2: x = x + 1; break; case 3: y = y + 1; break; case 4: break; } } moveCount = moveList[curX][curY]; movingList[x][y] = 0; return; } // 获得矫正的移动步数 moveCount = moveCount + map.getMapCost(curX, curY); if (movingList[curX][curY] > -1) { moveCount = movingList[curX][curY]; for (int j = 0; j <= maxY - 1; j++) { for (int i = 0; i <= maxX - 1; i++) { if (movingList[i][j] > movingList[curX][curY]) { movingList[i][j] = -1; } } } } movingList[curX][curY] = moveCount; } /** * 设定最短移动路径 * * @param x * @param y * @return */ public synchronized int setMoveCouse(int x, int y) { // 判定左方最短路径 if ((x > 0) && (moveList[x - 1][y] > -1) && (moveList[x - 1][y] < moveList[x][y]) && (moveList[x - 1][y] == moveCount - map.getMapCost(x, y))) { moveCount = moveCount - map.getMapCost(x, y); movingList[x - 1][y] = moveCount; return 0; } // 判定上方最短路径 if ((y > 0) && (moveList[x][y - 1] > -1) && (moveList[x][y - 1] < moveList[x][y]) && (moveList[x][y - 1] == moveCount - map.getMapCost(x, y))) { moveCount = moveCount - map.getMapCost(x, y); movingList[x][y - 1] = moveCount; return 1; } // 判定右方最短路径 if ((x < maxX - 1) && (moveList[x + 1][y] > -1) && (moveList[x + 1][y] < moveList[x][y]) && (moveList[x + 1][y] == moveCount - map.getMapCost(x, y))) { moveCount = moveCount - map.getMapCost(x, y); movingList[x + 1][y] = moveCount; return 2; } // 判定下方最短路径 if ((y < maxY - 1) && (moveList[x][y + 1] > -1) && (moveList[x][y + 1] < moveList[x][y]) && (moveList[x][y + 1] == moveCount - map.getMapCost(x, y))) { moveCount = moveCount - map.getMapCost(x, y); movingList[x][y + 1] = moveCount; return 3; } return 4; } /** * 移动角色 * */ public synchronized void moveRole() { state = "开始移动"; int x = lastX; int y = lastY; int direction; // 移动方向判定 for (int i = 0; i <= moveCount; i++) { direction = 4; if ((x > 0) && (moveList[x - 1][y] > -1) && (movingList[x - 1][y] - map.getMapCost(x - 1, y) == movingList[x][y])) direction = 0; // 左 if ((y > 0) && (moveList[x][y - 1] > -1) && (movingList[x][y - 1] - map.getMapCost(x, y - 1) == movingList[x][y])) direction = 1; // 上 if ((x < maxX - 1) && (moveList[x + 1][y] > -1) && (movingList[x + 1][y] - map.getMapCost(x + 1, y) == movingList[x][y])) direction = 2; // 右 if ((y < maxY - 1) && (moveList[x][y + 1] > -1) && (movingList[x][y + 1] - map.getMapCost(x, y + 1) == movingList[x][y])) direction = 3; // 下 switch (direction) { case 0: x = x - 1; break; case 1: y = y - 1; break; case 2: x = x + 1; break; case 3: y = y + 1; break; case 4: break; } Role role = getRole(actionUnit); role.setX(x); role.setY(y); drawBattle(); Utility.wait(10); } getRole(actionUnit).x = curX; getRole(actionUnit).y = curY; Utility.wait(10); } /** * 设定移动范围 */ public synchronized void setMoveRange() { Role role = getRole(actionUnit); int x = role.x; int y = role.y; int area = role.move; // 有效范围 moveList[x][y] = 0; // 设定现在为移动0步 for (int count = 0; count <= area - 1; count++) { for (int j = redressY(y - area); j < redressY(y + area); j++) { for (int i = redressX(x - (area - Math.abs(y - j))); i <= redressX(x + (area - Math.abs(y - j))); i++) { // 如果能够移动指定步数 if ((getMoveCount(i - 1, j) == count) || (getMoveCount(i, j - 1) == count) || (getMoveCount(i + 1, j) == count) || (getMoveCount(i, j + 1) == count)) { setMoveCount(i, j, count); } } } } area = area + 1; // 射程 for (int j = redressY(y - area); j <= redressY(y + area); j++) { for (int i = redressX(x - (area - Math.abs(y - j))); i <= redressX(x + (area - Math.abs(y - j))); i++) { // 远程攻击 if ((getMoveCount(i - 1, j) > -1) || (getMoveCount(i, j - 1) > -1) || (getMoveCount(i + 1, j) > -1) || (getMoveCount(i, j + 1) > -1)) { attackList[i][j] = 1; } } } } /** * 获得指定索引及分组下角色 * * @param team * @param x * @param y * @return */ public synchronized int getRoleIdx(final int team, final int x, final int y) { int index = 0; for (Iterator it = unitList.iterator(); it.hasNext();) { Role role = (Role) it.next(); if (x == role.x && y == role.y && team == role.team) { return index; } index++; } return -1; } /** * 创建角色 * * @param name * @param team * @param imageIndex * @param move * @param x * @param y */ private synchronized void createRole(String name, int team, int imageIndex, int move, int x, int y) { unitList.add(new Role(name, team, unitImages[imageIndex], move, x, y)); } /** * 绘制画面 * */ public synchronized void drawBattle() { int count = 0; // 绘制地图 graphics.drawImage(mapImage, 0, 0, null); // 移动范围绘制 if ((state.equalsIgnoreCase("角色移动")) || (state.equalsIgnoreCase("移动范围"))) { for (int j = 0; j <= maxY - 1; j++) { for (int i = 0; i <= maxX - 1; i++) { if (moveList[i][j] > -1) { graphics.drawImage(iconImages[2], i * tile, j * tile, null); } else if (attackList[i][j] > 0) { graphics.drawImage(iconImages[3], i * tile, j * tile, null); } } } } // 角色绘制 for (int index = 0; index < unitList.size(); index++) { Role role = (Role) unitList.get(index); if (index == actionUnit) { // 当前控制角色处理(此示例未加入特殊处理) graphics.drawImage(role.getImage(), role.getX() * tile, role .getY() * tile, null); } else { graphics.drawImage(role.getImage(), role.getX() * tile, role .getY() * tile, null); } // 已行动完毕 if (role.action == 1) { graphics.drawImage(unitImages[3], role.getX() * tile, role .getY() * tile, null); } } // 攻击范围绘制 if (state.equalsIgnoreCase("进行攻击")) { for (int j = 0; j <= maxY - 1; j++) { for (int i = 0; i <= maxX - 1; i++) { int result = attackList[i][j]; if (result == 2) { graphics.drawImage(iconImages[3], i * tile, j * tile, null); } // 标注选中的攻击对象 if (result == 2 && getRoleIdx(1, i, j) > -1 && curX == i && curY == j) { graphics.drawImage(iconImages[4], i * tile, j * tile, null); } } } } // 绘制移动路线 if (state.equalsIgnoreCase("角色移动")) { for (int j = 0; j <= maxY - 1; j++) { for (int i = 0; i <= maxX - 1; i++) { if (movingList[i][j] == -1) { continue; } count = 0; if ((movingList[i][j] == 0) || (movingList[i][j] == moveCount)) { if ((i > 0) && (movingList[i - 1][j] > -1) && ((movingList[i - 1][j] - map.getMapCost(i - 1, j) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i - 1][j]))) { count = 1; } if ((j > 0) && (movingList[i][j - 1] > -1) && ((movingList[i][j - 1] - map.getMapCost(i, j - 1) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i][j - 1]))) { count = 2; } if ((i < maxX-1) && (movingList[i + 1][j] > -1) && ((movingList[i + 1][j] - map.getMapCost(i + 1, j) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i + 1][j]))) { count = 3; } if ((j < maxY-1) && (movingList[i][j + 1] > -1) && ((movingList[i][j + 1] - map.getMapCost(i, j + 1) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i][j + 1]))) { count = 4; } if (movingList[i][j] != 0) { count = count + 4; } } else { count = 6; if ((i > 0) && (movingList[i - 1][j] > -1) && ((movingList[i - 1][j] - map.getMapCost(i - 1, j) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i - 1][j]))) { count = count + 1; } if ((j > 0) && (movingList[i][j - 1] > -1) && ((movingList[i][j - 1] - map.getMapCost(i, j - 1) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i][j - 1]))) { count = count + 2; } if ((i < maxX-1) && (movingList[i + 1][j] > -1) && ((movingList[i + 1][j] - map.getMapCost(i + 1, j) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i + 1][j]))) { count = count + 3; } if ((j < maxY-1) && (movingList[i][j + 1] > -1) && ((movingList[i][j + 1] - map.getMapCost(i, j + 1) == movingList[i][j]) || (movingList[i][j] - map.getMapCost(i, j) == movingList[i][j + 1]))) { count = count + 5; } } if (count > 0) { graphics.drawImage(iconImages[count + 4], i * tile, j * tile, null); } } } } // 菜单 if (menu.visible) { Utility.setAlpha(graphics, 0.50f); graphics.drawImage(listImages[0], menu.getLeft(curX) * tile, 0, null); for (int i = 1; i <= menu.width; i++) { graphics.drawImage(listImages[1], (menu.getLeft(curX) + i) * tile, 0, null); } graphics.drawImage(listImages[2], (menu.getLeft(curX) + menu.width + 1) * tile, 0, null); for (int j = 1; j <= menu.height; j++) { graphics.drawImage(listImages[3], menu.getLeft(curX) * tile, j * tile, null); for (int i = 1; i <= menu.width; i++) { graphics.drawImage(listImages[4], (menu.getLeft(curX) + i) * tile, j * tile, null); } graphics.drawImage(listImages[5], (menu.getLeft(curX) + menu.width + 1) * tile, j * tile, null); } graphics.drawImage(listImages[6], menu.getLeft(curX) * tile, (menu.height + 1) * tile, null); for (int i = 1; i <= menu.width; i++) { graphics.drawImage(listImages[7], (menu.getLeft(curX) + i) * tile, (menu.height + 1) * tile, null); } graphics.drawImage(listImages[8], (menu.getLeft(curX) + menu.width + 1) * tile, (menu.height + 1) * tile, null); Utility.setAlpha(graphics, 1.0f); // 写入文字 graphics.drawImage(iconImages[1], (menu.getLeft(curX) + 1) * tile, (menu.cur + 1) * tile, null); for (int j = 1; j <= menu.height; j++) { graphics.setColor(Color.white); Utility.drawDefaultString(menu.getMenuItem(j - 1), graphics, (menu.getLeft(curX) + 2) * tile, ((j * tile)) + 24, 0, 23); } } // 显示状态 if (state.equalsIgnoreCase("状态显示")) { int i = getRoleIdx(0, curX, curY); if (i == -1) { i = getRoleIdx(1, curX, curY); } if (i > -1) { Role role = (Role) unitList.get(i); Utility.setAlpha(graphics, 0.75f); graphics.drawImage(listImages[0], menu.getLeft(curX) * tile, 0, null); graphics.drawImage(listImages[1], (menu.getLeft(curX) + 1) * tile, 0, null); graphics.drawImage(listImages[1], (menu.getLeft(curX) + 2) * tile, 0, null); graphics.drawImage(listImages[2], (menu.getLeft(curX) + 3) * tile, 0, null); graphics.drawImage(listImages[3], (menu.getLeft(curX)) * tile, tile, null); graphics.drawImage(listImages[4], (menu.getLeft(curX) + 1) * tile, tile, null); graphics.drawImage(listImages[4], (menu.getLeft(curX) + 2) * tile, tile, null); graphics.drawImage(listImages[5], (menu.getLeft(curX) + 3) * tile, tile, null); graphics.drawImage(listImages[3], menu.getLeft(curX) * tile, 64, null); graphics.drawImage(listImages[4], (menu.getLeft(curX) + 1) * tile, 64, null); graphics.drawImage(listImages[4], (menu.getLeft(curX) + 2) * tile, 64, null); graphics.drawImage(listImages[5], (menu.getLeft(curX) + 3) * tile, 64, null); graphics.drawImage(listImages[6], (menu.getLeft(curX)) * tile, 96, null); graphics.drawImage(listImages[7], (menu.getLeft(curX) + 1) * tile, 96, null); graphics.drawImage(listImages[7], (menu.getLeft(curX) + 2) * tile, 96, null); graphics.drawImage(listImages[8], (menu.getLeft(curX) + 3) * tile, 96, null); Utility.setAlpha(graphics, 1.0f); // 显示角色数据 graphics.drawImage(role.getImage(), (menu.getLeft(curX) + 1) * tile + 16, tile, null); Utility.drawDefaultString("HP:" + role.getHp(), graphics, (menu .getLeft(curX) + 1) * tile + 12, 75, 1, 12); Utility.drawDefaultString("MV:" + role.getMove(), graphics, (menu.getLeft(curX) + 1) * tile + 12, 88, 1, 12); } } // 战斗回合 if (state.equalsIgnoreCase("战斗开始") || state.equalsIgnoreCase("战斗结束")) { Utility.setAlpha(graphics, 0.5f); graphics.setColor(Color.black); graphics.fillRect(0, 90, 320, 140); graphics.setColor(Color.white); Utility.setAlpha(graphics, 1.0f); Utility.drawDefaultString("第" + turn + "回合", graphics, 120, 160, 0, 25); } // 我方移动 else if (state.equalsIgnoreCase("开始移动")) { // 未添加处理 } else if (state.equalsIgnoreCase("敌方行动")) { for (int i = unitList.size() - 1; i > -1; i--) { Role role = (Role) unitList.get(i); // 敌军,且无法再次移动和攻击 if (role.team == 1 && role.action == 1) { int x = role.x; int y = role.y; int index = 0; // 当敌军移动地点附近才能在我方人物时, 直接删除List中我方角色(实际开发中应加入相应判定) if ((index = getRoleIdx(0, x, y + 1)) > -1 && !role.isAttack()) { unitList.remove(index); } else if ((index = getRoleIdx(0, x, y - 1)) > -1 && !role.isAttack()) { unitList.remove(index); } else if ((index = getRoleIdx(0, x + 1, y)) > -1 && !role.isAttack()) { unitList.remove(index); } else if ((index = getRoleIdx(0, x - 1, y)) > -1 && !role.isAttack()) { unitList.remove(index); } role.setAttack(true); } } } else { // 绘制光标 graphics.drawImage(iconImages[0], curX * tile, curY * tile, null); } // 刷新画面 this.repaint(); } public void paint(Graphics g) { g.drawImage(screen, 0, 0, null); g.dispose(); } public void update(Graphics g) { paint(g); } /** * 矫正x坐标 * * @param x * @return */ public synchronized int redressX(int x) { if (x < 0) x = 0; if (x > maxX - 1) x = maxX - 1; return x; } /** * 矫正y坐标 * * @param y * @return */ public synchronized int redressY(int y) { if (y < 0) y = 0; if (y > maxY - 1) y = maxY - 1; return y; } /** * 敌军行动 * */ public synchronized void enemyAction() { for (int index = 0; index < unitList.size(); index++) { Role role = (Role) unitList.get(index); if (role.team != 1) { continue; } actionUnit = index; setMoveRange(); // 随机选择敌方移动地点 int x = role.move - new Random().nextInt(role.move * 2 + 1); int y = (role.move - Math.abs(x)) - new Random().nextInt((role.move - Math.abs(x)) * 2 + 1); x = redressX(role.x + x); y = redressY(role.y + y); if ((moveList[x][y] > 0) && (getRoleIdx(0, x, y) == -1) && (getRoleIdx(1, x, y) == -1)) { // 记录角色最后的移动位置 lastX = role.x; lastY = role.y; curX = x; curY = y; moveCount = moveList[x][y]; movingList[x][y] = moveCount; for (int i = 0; i < moveCount; i++) { switch (setMoveCouse(x, y)) { case 0: x = x - 1; break; case 1: y = y - 1; break; case 2: x = x + 1; break; case 3: y = y + 1; break; default: break; } } moveCount = moveList[curX][curY]; movingList[x][y] = 0; moveRole(); } state = "敌方行动"; curX = 0; curY = 0; role.setAction(1); role.setAttack(false); actionUnit = -1; initRange(); drawBattle(); Utility.wait(200); } } public void keyReleased(KeyEvent e) { if (state.equalsIgnoreCase("战斗开始")) return; if (state.equalsIgnoreCase("战斗结束")) return; if (state.equalsIgnoreCase("敌方行动")) return; // 菜单可见 if (menu.visible) { switch (e.getKeyCode()) { case KeyEvent.VK_UP: if (menu.cur > 0) { menu.cur = menu.cur - 1; } break; case KeyEvent.VK_DOWN: if (menu.cur < menu.height - 1) { menu.cur = menu.cur + 1; } break; } } // 菜单不可见 else { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: curX = redressX(curX - 1); break; case KeyEvent.VK_UP: curY = redressY(curY - 1); break; case KeyEvent.VK_RIGHT: curX = redressX(curX + 1); break; case KeyEvent.VK_DOWN: curY = redressY(curY + 1); break; } } if (state.equalsIgnoreCase("角色移动")) { setMoveCourse(); } drawBattle(); } public void keyPressed(KeyEvent e) { int code = e.getKeyCode(); eventCode = code; } public void keyTyped(KeyEvent e) { } }

示例程序截图如下:

回合开始:

角色状态:

移动寻径:

菜单交互:

目标选择:

活动单元转移:

代码下载地址:http://download.csdn.net/source/1047937

————华丽的分割线———

本来上周就说写的东西,却由于某个事件的刺激,导致上周某几天中回家就跑去各个论坛跟水军打嘴仗,拖到本周才动手垒码……对于这种“嘴勤屁股懒”的行径,在此强烈鄙视自己(-_-|||)……

PS:实际上示例代码周二晚已完成,计划中昨晚就该发博文,结果中途忍不住又点了某个论坛,又和水军对喷半天,所以耗到今天这篇博文才得以面世,写的不够周全,这两天会慢慢补齐,还望各位大人见谅^^。


Java版战棋(SLG)游戏AI及寻径处理入门相关推荐

  1. 从零点五开始用Unity做半个2D战棋小游戏(完)

    好久不见. 这是第29篇与游戏开发有关的文章. | 写在最前 这次想要做一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方在地图上轮流行动,并向对方发动 ...

  2. 从零点五开始用Unity做半个2D战棋小游戏(十一)

    好久不见. 这是第28篇与游戏开发有关的文章. | 写在最前 这次想要做一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方在地图上轮流行动,并向对方发动 ...

  3. 从零点五开始用Unity做半个2D战棋小游戏(十)

    好久不见. 这是第27篇与游戏开发有关的文章. | 写在最前 这次想要做一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方在地图上轮流行动,并向对方发动 ...

  4. 从零点五开始用Unity做半个2D战棋小游戏(九)

    写在最前 这次想要做一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方在地图上轮流行动,并向对方发动攻击,先消灭掉所有敌人的一方将获得胜利. 预计将分为 ...

  5. 从零点五开始用Unity做半个2D战棋小游戏(八)

    写在最前 这次想要一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方在地图上轮流行动,并向对方发动攻击,先消灭掉所有敌人的一方将获得胜利. 预计将分为以 ...

  6. 从零点五开始用Unity做半个2D战棋小游戏(七)

    这是第24篇与游戏开发有关的文章. | 写在最前 这次想要一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方依据体力在地图上轮流行动并向对方发动攻击,先 ...

  7. html战旗游戏,战棋页游-策略类战棋网页游戏推荐

    战棋类游戏是最经典的一种策略游戏类型,由于战棋游戏每个武将限制了移动范围,活动力非常受限制,因此策略性更强,有时候真的就是差一步移动力导致全军覆没.经典的战棋单机游戏还是很多的,本站上面的三国志曹操传 ...

  8. 从零点五开始用Unity做半个2D战棋小游戏(六)

    好久不见. 这是第23篇与游戏开发有关的文章. | 写在最前 这次想要一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方依据体力在地图上轮流行动并向对方 ...

  9. 从零点五开始用Unity做半个2D战棋小游戏(五)

    写在最前 这次想要一个简单且传统的战棋小游戏,大概的玩法是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方依据体力在地图上轮流行动并向对方发动攻击,先消灭掉所有敌人的一方将获得胜利. 预计将 ...

最新文章

  1. DirectX 90 3D 外接体
  2. SVN项目,快速查看项目的当前版本号
  3. TCP三次握手及关闭时的2MSL分析
  4. itext 添加空格_itext7史上最全实战总结
  5. 2013浙大878计算机基础综合大题答案解析
  6. windows共享关闭密码保护是灰色的
  7. 用户登录问题python_Python Flask单点登录问题
  8. 树莓派7寸触屏,略贵
  9. 服装店商家不离手的十大服装进销存管理软件,功能大对比
  10. matlab多行注释
  11. DataWhale-动手数据分析-Task01:数据加载及探索性数据分析
  12. 跨境必看:跨境支付问题以及热门跨境支付方式的优劣势分析!
  13. 2017最新申请苹果开发者账号,申请appstore开发者账号,申请app开发者账号,申请苹果企业公司开发者账号
  14. css渐变小案例,比较复杂的线性渐变和径向渐变做泡泡
  15. 这些愚蠢的事 数据中心管理中要避免
  16. OA系统,满足各行业办公所需的管理软件
  17. 新年新故事 | Nice 兔 Meet U
  18. 2019最新《Python从入门到精通之30天快速学Python教程 》
  19. html网页载入后焦点,HTML5中,用于指定页面加载后是否自动获取焦点的input属性是 答案:autofocus属性...
  20. 课程7 :PLC常见指令详解:比较指令(工控PLC工程师入门必读,5天可上手调试)

热门文章

  1. PHP,$this-{$xxx} 是什么意思?
  2. android微单,用相机打电话 国产安卓系统微单了解一下
  3. 【基础】光滑曲线什么意思?以及 n次方差、n次方和公式、二项式定理(和的n次方)
  4. 面试指南-----项目自我介绍
  5. Log4j2跨线程打印traceId
  6. Unity Shader-Ambient Occlusion环境光遮蔽(AO贴图,GPU AO贴图烘焙,SSAO,HBAO)
  7. qq离线linux,QQ For Linux 我哭了,官方版
  8. 杂散干扰解决办法_F频段干扰问题的几种解决方案
  9. [原][连载]那时花开(三)
  10. 3G杀手系列之一:寻找真正3G杀手