上一节课中,我们讲述了SLG中获取移动范围的算法(获取攻击范围也是同理),相对如自动寻径来说,简单不少。由于个人时间问题,这一节课将会把内容讲完,将这个系列完结,并给出示例下载地址。
项目下载地址:JavaFX战旗类游戏开发示例
注意:该项目为e(fx)clipse项目
在战旗游戏开发中,最基本的回合逻辑就是敌方回合和我方回合。当然,在如今的SLG游戏中,往往是根据我方角色和敌方角色的某些数值计算(例如速度之类的),来排列角色操控的列表,而且某些技能还能中断某个角色的操作,将他往操作列表的后面移动(例如著名的SLG游戏《英雄传说》)。
当然,在这里我们只是讲述最简单的回合逻辑。
下面是我们需要定义的一些简单枚举:
// 游戏状态 enum Status { NONE, SHOW_ENEMY_PROPERTY, SHOW_MENU, PREPARE_MOVE, MOVE, PREPARE_ATTACK, ATTACK, WAIT, GAME_WIN, GAME_OVER } enum GameTurn { PLAYER, ENEMY } // 当前游戏状态 private Status nowStatus = Status.NONE; // 当前游戏回合 private GameTurn nowTurn = GameTurn.PLAYER;
我们这里的Status里包含游戏状态:显示敌方属性,显示菜单,准备移动,移动,准备攻击,攻击,等待,游戏胜利,游戏失败。
另外,定义了一个回合枚举,分为我方回合和敌方回合。当然,有的SLG中阵营分类不止两个,例如以前的FC上的《第四次机器人大战》,里面有我方,敌方和中立阵营(游戏中的名字叫联邦,自盟和什么来着,忘了。这个游戏是外星科技山寨改版的)。
接下来,上一课中,我们创建的定时器就要派上用场了。
moveTimer = WTimer.createWTimer(50, new WTimer.OnTimerListener() { @Override public void onTimerRunning(WTimer mTimer) { if (nowStatus == Status.MOVE) { int nowPlayerX = (int) (nowControllPlayer.getX() / tileWidth); int nowPlayerY = (int) (nowControllPlayer.getY() / tileHeight); if (nowPlayerX != moveToX) { nowControllPlayer.moveX(nowPlayerX > moveToX ? -tileWidth : tileWidth); } else { if (nowPlayerY != moveToY) { nowControllPlayer.moveY(nowPlayerY > moveToY ? -tileHeight : tileHeight); } else { nowControllPlayer.setWaitToAttack(false); nowControllPlayer.setWaitToMove(false); nowControllPlayer.setCanMove(false); nowControllPlayer = null; nowStatus = Status.NONE; moveTimer.stop(); } } } } }); actioTimer = WTimer.createWTimer(50, new WTimer.OnTimerListener() { @Override public void onTimerRunning(WTimer mTimer) { //敌方回合 if (nowTurn == GameTurn.ENEMY) { if (nowStatus == Status.NONE) { //当前敌方角色索引 if (nowActionIndex < enemys.size()) { //选择操作的敌方角色 nowControllPlayer = enemys.get(nowActionIndex); nowControllPlayer.setChoose(true); // 没有就近角色 if (!nowControllPlayer.isHasNearBP(players)) { //如果可以移动 if (nowControllPlayer.isCanMove()) { //获取最近的一个角色(这里可以更改规则,比如HP最低的等等) BasePlayer player = nowControllPlayer.getNearestBP(players); path.clear(); // path.map_sprite = createMapSprite(); //搜索移动范围 LinkedList<WNode> nodeList = path.SearchMoveScan( new Point2D(nowControllPlayer.getX() / tileWidth, nowControllPlayer.getY() / tileHeight), nowControllPlayer.getMove()); // 删选当前可移动范围内 如果有角色,则把该移动点删除 List<WNode> deleteList = new ArrayList<>(); for (WNode node : nodeList) { if (isPointHasPlayer((int) node.getPoint().getX(), (int) node.getPoint().getY())) { deleteList.add(node); } } for (WNode node : deleteList) { nodeList.remove(node); } nowControllPlayer.nodeList = nodeList; //获取可移动范围里距离 最近角色的最近的点 Point2D point = player.getNearestNode(nodeList); moveToX = (int) (point.getX()); moveToY = (int) (point.getY()); //状态更改为移动 nowStatus = Status.MOVE; moveTimer.start(); } else { //状态更改为准备攻击 nowStatus = Status.PREPARE_ATTACK; } } else { //状态更改为准备攻击 nowStatus = Status.PREPARE_ATTACK; } } else { //当操作角色的索引大于 敌方角色集合的大小时,回合结束 并重置所有敌方角色状态 nowStatus = Status.NONE; nowTurn = GameTurn.PLAYER; nowActionIndex = 0; for (BasePlayer enemy : enemys) { enemy.reset(); } } } else if (nowStatus == Status.PREPARE_ATTACK) { // 敌人周边四个方格是否有角色 if (nowControllPlayer.isHasNearBP(players)) { //敌人获取最近的我方角色,并攻击 BasePlayer bp = nowControllPlayer.getNearestBP(players); nowControllPlayer.attack(bp); nowBeAttackedPlayer = bp; bp.setFlash(true); nowStatus = Status.ATTACK; } else { //没有攻击对象,则操作下一个敌方角色 nowControllPlayer.setCanAction(false); nowActionIndex++; nowControllPlayer.setChoose(false); nowStatus = Status.NONE; } } else if (nowStatus == Status.ATTACK) { //角色死亡 if (!nowBeAttackedPlayer.isFlash()) { if (nowBeAttackedPlayer.getHp() <= 0) { players.remove(nowBeAttackedPlayer); } nowControllPlayer.setCanAction(false); nowActionIndex++; nowControllPlayer.setChoose(false); nowStatus = Status.NONE; //当没有我方角色时,游戏结束 if (players.size() == 0) { nowStatus = Status.GAME_OVER; nowTurn = GameTurn.PLAYER; } } } } else if (nowTurn == GameTurn.PLAYER) { if (nowStatus == Status.ATTACK) { if (!nowBeAttackedPlayer.isFlash()) { //敌人死亡 if (nowBeAttackedPlayer.getHp() <= 0) { nowControllPlayer.getExp(nowBeAttackedPlayer.getExp()); enemys.remove(nowBeAttackedPlayer); } waitToNextPlayer(); //当没有敌方角色时,游戏胜利 if (enemys.size() == 0) { nowStatus = Status.GAME_WIN; } } } } } }); actioTimer.start();
MoveTimer的作用是控制角色的移动(根据时间间隔一个单元格一个单元格的移动)。
ActionTimer的作用是敌方回合或者我方回合的一些操作。
由于我方回合主要是菜单的鼠标事件和简单的判断,这里我描述一下敌方回合的步骤。
敌方回合
1.当前敌方操作角色索引小于敌方角色链表大小,如果CanMove,则获取最近的我方角色(也可以根据HP等来判断),并获取敌方角色可移动的范围。
2.从敌方角色可移动范围中,获取距离“上个步骤中得到的我方最角色”最近的一个点,并将状态改为Move,启动MoveTimer开始移动。
3.如果!CanMove(就是已经移动过了),则将状态更改为PREPARE_ATTACK,准备攻击。
4.如果状态为PREPARE_ATTACK,则判断敌方周围四个单元格有无可攻击对象,如果有,则获取对象并将状态更改为ATTACK并攻击,如果无,则操作下一个敌方角色。
5.如果状态为ATTACK,则判断被攻击的对象是否死亡,死亡则移出链表。当我方角色链表为空,则游戏结束。
以上为敌方回合的逻辑步骤,代码中的注释比较多,就不做过多说明。可以对照项目代码来看。
然后,在鼠标事件和绘制中,也是通过状态来管理的,这里列出绘制代码来说明。
public void draw() { gameMap.drawMap(gContext); drawPlayer(); switch (nowStatus) { case SHOW_MENU: if (nowControllPlayer != null && nowControllPlayer.isCanAction()) { actionMenu.draw(gContext); } propertyMenu.draw(gContext); break; case SHOW_ENEMY_PROPERTY: propertyMenu.draw(gContext); break; case PREPARE_MOVE: break; case GAME_WIN: gContext.setFont(Font.font(18)); gContext.setFill(Color.WHITE); gContext.fillText("游戏胜利!", 250, 150); break; case GAME_OVER: gContext.setFont(Font.font(18)); gContext.setFill(Color.RED); gContext.fillText("游戏失败!", 250, 150); break; default: break; } gContext.save(); gContext.setFont(Font.font(18)); switch (nowTurn) { case PLAYER: gContext.setFill(Color.WHITE); gContext.fillText("我方回合", 15, getHeight() - 15); break; case ENEMY: gContext.setFill(Color.RED); gContext.fillText("敌方回合", 15, getHeight() - 15); break; } gContext.restore(); }
在绘制中,我们根据不同的状态来绘制不同的内容。
鼠标事件原理是如此,如下所示:
// 事件处理 setOnMouseClicked(e -> { if (e.getButton() == MouseButton.PRIMARY) { switch (nowStatus) { case NONE: for (BasePlayer player : players) { //判断点击的角色,显示属性框和操作菜单 if (player.isCollisionWith(e.getX(), e.getY())) { actionMenu.setLocation(player.getX() + tileWidth, player.getY()); actionMenu.getTextObjects()[0].setColor(player.isCanMove() != true ? Color.DARKGRAY : Color.WHITE); actionMenu.getTextObjects()[1].setColor(player.isCanAttack() != true ? Color.DARKGRAY : Color.WHITE); propertyMenu.initPlayer(player); nowControllPlayer = player; nowControllPlayer.setChoose(true); nowStatus = Status.SHOW_MENU; } } //如果点击的是敌方角色,则只显示敌方属性框 for (BasePlayer enemy : enemys) { if (enemy.isCollisionWith(e.getX(), e.getY())) { propertyMenu.initPlayer(enemy); nowStatus = Status.SHOW_ENEMY_PROPERTY; } } break; case SHOW_MENU: actionMenu.onMousePressed(e); break; case PREPARE_MOVE: moveToX = (int) (e.getX() / tileWidth); moveToY = (int) (e.getY() / tileHeight); isCanMove = false; // 判断点击的是否是可移动的范围 if (!isPointHasPlayer(moveToX, moveToY)) { for (WNode node : nowControllPlayer.nodeList) { if (((int) node.getPoint().getX()) == moveToX && ((int) node.getPoint().getY()) == moveToY) { isCanMove = true; } } //如果可以移动,则启动移动定时器 if (isCanMove) { nowStatus = Status.MOVE; moveTimer.start(); nowControllPlayer.setChoose(false); } } break; case PREPARE_ATTACK: //准备攻击状态时,点击要攻击的角色 for (BasePlayer enemy : enemys) { if (enemy.isCollisionWith(e.getX(), e.getY())) { nowControllPlayer.attack(enemy); nowControllPlayer.setWaitToAttack(false); enemy.setFlash(true); nowBeAttackedPlayer = enemy; nowStatus = Status.ATTACK; } } break; default: break; } } else if (e.getButton() == MouseButton.SECONDARY) { //右键状态还原 if (nowControllPlayer != null) { nowControllPlayer.setChoose(false); nowControllPlayer.setWaitToAttack(false); nowControllPlayer.setWaitToMove(false); } nowStatus = Status.NONE; nowControllPlayer = null; } });
具体的大家可以研究项目代码。
截图如下:
本文章为个人原创,版权所有,转载请注明出处:http://blog.csdn.net/ml3947。另外我的个人博客:http://www.wjfxgame.com.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
其实这个示例已经完成很久了,之前是一个CSDN的朋友说想看看JavaFX战旗类游戏开发的示例。于是业余花了一点点时间写了这个例子。随后开始写了这个《JavaFX战旗类游戏开发》的系列博文,可惜,中间间间断断时不时写一篇,已经过去了好几个月了,汗,感觉自己变得有点懒散了,于是总共七课的课程算是完结了,之前的系列文章有很多坑,不知道何时会想到去填。。今年下半年算是不平凡的半年,考了驾照,然后结婚,老婆家酒已经办了,我家还没办。之后还要度蜜月,空余的时间也没多少心思写着写那,无奈。
另外,很多朋友发私信加QQ。但是现在本人QQ非特殊情况基本不加技术讨论的。毕竟有时候还是想休息之类的。如果有任何问题发我的邮件Gmail:[email protected]。谢谢。
--------------------------------------------------------------------------------------------------------------