JavaFX 2 GameTutorial第3部分
Ť他是与一个六个部分组成的系列的第3部分的JavaFX 2游戏教程。 如果您错过了第1部分和第2部分 ,建议您在开始本教程之前先进行阅读。 回顾第二部分,我讨论了游戏循环的内部工作原理,其中我们使用动画(JavaFX Timeline )更新精灵,检查碰撞并清理游戏世界元素,然后我不得不创建一个简单的游戏引擎以简化操作开发2D游戏。 本教程是关于使用游戏引擎并使用鼠标和键盘来演示输入的。在本教程中,我将为您提供一些背景历史记录,事件处理基础知识,演示游戏以及最后的实现。 该演示将展示一艘能够在漂浮的球形射击的太空船,类似于电子游戏“ 小行星” 。 如果要运行演示,请向下滚动并单击下面的WebStart按钮。 在启动游戏之前,请先阅读要求。
历史
在上世纪80年代那年小时候,这里有拱廊中心,保龄球馆,披萨店和7家11家商店,我花了大量时间在玻璃展示区摆放着四分之一的空间,以便与玻璃陈列室相邻。目前正在玩激烈的电子游戏的家伙。 当每个人都拥挤在他身边时,看着他击败了所有的最高分,当我们看到伟大时,我们所有人都为之欢呼。 其中的一个令人难以置信的真棒街机游戏是“ 小行星 ”由Atari公司创建(打参观play.vg )
说到高分,并不是很多人都知道,但是斯科特·赛峰 ( Scott Safran) (1967年2月3日至1989年3月27日)是有史以来玩小行星的最高记录。 他在当地的7-11便利店里连续玩了大约二十个小时才做到了这一点。 在生命的后期(还很小的时候),他在1989年3月27日的一次不幸事故中去世。为了纪念Scott,我创建了本教程。 我希望人们会记得他是有史以来最伟大的视频游戏玩家之一(我敢肯定,他也是个好兄弟)。
关于游戏,小行星基于矢量的硬件用于渲染形状,而不是栅格图形(位图)。 另外,使用光栅图形创建了Midway Inc.的Space Invaders。 令人兴奋的是,有关于JavaFX 2.x能够使用称为JavaFX Canvas Node的位图的讨论,该位图可以提供栅格图形以使开发人员能够利用像素级操纵。 这些街机风格的机柜的结构仍然让我感到惊讶,这些机柜内装有CRT,主板和诸如按钮,操纵杆,跟踪球和旋钮之类的控制器(输入设备)。
经典街机游戏
以下是一些具有多种输入设备的经典街机游戏 :
- 仅按钮 :小行星,太空侵略者,翻录,凤凰
- 仅操纵杆 :Q * bert,PacMan
- 仅旋钮 :Pong
- 仅轨迹球 :大理石疯狂
- 操纵杆和按钮 : 星球大战 ,杆位,间谍猎人
- 自行车车把 :特技自行车,纸男孩
- 按钮和油门杆 :月球着陆器
- 潜望镜和按钮 :海狼
- 按钮和轭 :Tron,战区
- 按钮,旋钮和拨叉 :《星际迷航》,《暴风雨》
- 按钮和轨迹球 :导弹司令部,enti
- 按钮和操纵杆 :防御者,护手,蛙人,乔斯特,狂风,马里奥兄弟,金刚,西捷,加拉加,功夫,魂斗罗,街头霸王,双龙,忍者魔法(或精神),挖土,龙之巢穴。
输入/(鼠标,键盘)
抛开过去,我们目前遇到了新型的输入设备,例如触摸屏,加速度计,红外接收器,照相机等。当今台式机上最常见的输入是鼠标和键盘。 当然,触摸屏已广泛应用于移动设备和平板电脑,但是在本教程中,我们将仅着眼于“ 鼠标 ”和“ 键盘 ”作为控制游戏的输入。 基于JavaFX路线图 ,正在进行多点触摸输入 (在您阅读本文时,它已经实现了)。
侦听键盘和鼠标事件时,JavaFX 2.x具有许多类型的事件,这为开发人员提供了机会来实现侦听触发事件的事件处理程序。 用于节点或场景的JavaFX 2.x API包含许多带有前缀“ on”的方法,例如onMousePressProperty()或onKeyPressProperty()方法。 无论何时实现这些方法,都将使用Java的泛型类型简单地实现handle()方法,以指定要传递给查询的事件对象。 因此,当实例化EventHandler <MouseEvent>类时,将实现一个handle()方法,该方法将MouseEvent作为要传入的参数。
下面显示的代码段将两个事件处理程序添加到JavaFX Scene。 第一个处理程序将响应鼠标事件。 在我们的简单游戏中,当发生鼠标按下时,该处理程序将通过发射武器或在飞船上进行响应。 下面显示的第二个处理程序将响应按键事件。 当按下一个键时,此处理程序将处理KeyEvent对象。 在我们的游戏中,按键“ 2 ”会将您的辅助武器变成更大的冲击波(更慢)。 其他任何击键将默认返回到较小的冲击波(更快)。
移动船和火武器
EventHandler fireOrMove = new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (event.getButton() == MouseButton.PRIMARY) { // Fire weapon systems. On Windows left mouse button } else if (event.getButton() == MouseButton.SECONDARY) { // Navigate ship thrust. On Windows right mouse button } } }; primaryStage.getScene().setOnMousePressed(fireOrMove);
更换武器
EventHandler changeWeapons = new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { myShip.changeWeapon(event.getCode()); } }; primaryStage.getScene().setOnKeyPressed(changeWeapons);
JavaFX 2输入演示–“扩展”
简单的演示游戏将是《星际争霸》和《小行星》之间的混合体。 使用鼠标导航飞船时,它将类似于《星际争霸》的《 战舰巡洋舰》 。 如果您还记得本系列文章的第2部分,那么我创建了球体反弹。 我重用了第2部分“原子粉碎机”中的代码,像著名的街机游戏一样充当小行星。 除了在这个游戏中,您根本不会受到伤害。 目的是在您的武器撞到撞击后会爆裂的其他球体之前向它们发射武器。 由于这是一个简单的教程,甚至是处于开发初期的游戏,因此该游戏无法跟踪得分。 我鼓励您去GitHub下载代码并增强游戏。 稍后,您将看到一个高级UML类图,该图描述了组成游戏的类。 为了简洁起见,我不会详细介绍每个类,但是我相信您会在这里访问GitHub: https : //github.com/carldea/JFXGen,以获取所有演示和源代码。
要求 :
- Java 7或更高版本
- JavaFX 2.1或更高版本
- Windows XP或更高版本(应该很快可用于Linux / MacOS)
一个简单的小行星类型游戏,名为“ The Expanse”。
说明:
- 右键单击(在Windows上)以飞船。
- 单击鼠标左键(在Windows鼠标上单击鼠标左键)即可射击武器。
- 按键'2? 变成大型导弹。(蓝色圆形弹丸)
- 其他按键默认为较小的导弹。 (红色圆形弹丸)
下面显示的是高级类图的图2,该图描述了为此演示创建的所有类。 GameWorld和Sprite类是上一篇文章的游戏引擎的一部分。 其余的类是新的,它们构成了本教程的演示。
InputPart3
InputPart3是运行游戏的驱动程序或主要JavaFX应用程序。 这将创建一个要初始化的GameWorld对象,并开始游戏循环。
下面显示的是主要JavaFX应用程序Input Part3的源代码。
import carlfx.gameengine.GameWorld; package carlfx.demos.navigateship; import javafx.application.Application; import javafx.stage.Stage; /** * The main driver of the game. * @author cdea */ public class InputPart3 extends Application { GameWorld gameWorld = new TheExpanse(59, "JavaFX 2 GameTutorial Part 3 - Input"); /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { // setup title, scene, stats, controls, and actors. gameWorld.initialize(primaryStage); // kick off the game loop gameWorld.beginGameLoop(); // display window primaryStage.show(); } }
广袤
TheExpanse类继承自GameWorld类。 这实际上与第2部分的“ AtomSmasher”相同,在该部分中,驱动程序应用程序将调用GameWorld实例的initialize()方法来设置所有游戏元素,例如input , spaceship和那些讨厌的浮动球体 。 此类任务是确保小行星或球体从墙壁反弹,并清除到达屏幕边缘的所有导弹。 负责任的主要是管理资产并创建新级别。 当没有移动物体并且玩家在屏幕上移动飞船时,将为下一个级别生成新的球体。该类的关键是setupInput()方法。 我创建的setupInput()方法负责建立事件处理程序,使其能够侦听键事件和鼠标事件。
package carlfx.demos.navigateship; import carlfx.gameengine.GameWorld; import carlfx.gameengine.Sprite; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.CacheHint; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.stage.Stage; import java.util.Random; /** * This is a simple game world simulating a bunch of spheres looking * like atomic particles colliding with each other. When the game loop begins * the user will notice random spheres (atomic particles) floating and * colliding. The user will navigate his/her ship by right clicking the mouse to * trust forward and left click to fire weapon to atoms. * * @author cdea */ public class TheExpanse extends GameWorld { // mouse pt label Label mousePtLabel = new Label(); // mouse press pt label Label mousePressPtLabel = new Label(); TextField xCoordinate = new TextField("234"); TextField yCoordinate = new TextField("200"); Button moveShipButton = new Button("Rotate ship"); Ship myShip = new Ship(); public TheExpanse(int fps, String title) { super(fps, title); } /** * Initialize the game world by adding sprite objects. * * @param primaryStage The game window or primary stage. */ @Override public void initialize(final Stage primaryStage) { // Sets the window title primaryStage.setTitle(getWindowTitle()); //primaryStage.setFullScreen(true); // Create the scene setSceneNodes(new Group()); setGameSurface(new Scene(getSceneNodes(), 800, 600)); getGameSurface().setFill(Color.BLACK); primaryStage.setScene(getGameSurface()); // Setup Game input setupInput(primaryStage); // Create many spheres generateManySpheres(2); // Display the number of spheres visible. // Create a button to add more spheres. // Create a button to freeze the game loop. //final Timeline gameLoop = getGameLoop(); getSpriteManager().addSprites(myShip); getSceneNodes().getChildren().add(myShip.node); // mouse point VBox stats = new VBox(); HBox row1 = new HBox(); mousePtLabel.setTextFill(Color.WHITE); row1.getChildren().add(mousePtLabel); HBox row2 = new HBox(); mousePressPtLabel.setTextFill(Color.WHITE); row2.getChildren().add(mousePressPtLabel); stats.getChildren().add(row1); stats.getChildren().add(row2); // mouse point HBox enterCoord1 = new HBox(); enterCoord1.getChildren().add(xCoordinate); enterCoord1.getChildren().add(yCoordinate); enterCoord1.getChildren().add(moveShipButton); stats.getChildren().add(enterCoord1); moveShipButton.setOnAction(new EventHandler() { @Override public void handle(ActionEvent actionEvent) { double x = Double.parseDouble(xCoordinate.getText()); double y = Double.parseDouble(yCoordinate.getText()); myShip.plotCourse(x, y, false); } }); // =================================================== // Debugging purposes // uncomment to test mouse press and rotation angles. //getSceneNodes().getChildren().add(stats); } /** * Sets up the mouse input. * * @param primaryStage The primary stage (app window). */ private void setupInput(Stage primaryStage) { System.out.println("Ship's center is (" + myShip.getCenterX() + ", " + myShip.getCenterY() + ")"); EventHandler fireOrMove = new EventHandler() { @Override public void handle(MouseEvent event) { mousePressPtLabel.setText("Mouse Press PT = (" + event.getX() + ", " + event.getY() + ")"); if (event.getButton() == MouseButton.PRIMARY) { // Aim myShip.plotCourse(event.getX(), event.getY(), false); // fire Missile m1 = myShip.fire(); getSpriteManager().addSprites(m1); getSceneNodes().getChildren().add(0, m1.node); } else if (event.getButton() == MouseButton.SECONDARY) { // determine when all atoms are not on the game surface. Ship should be one sprite left. if (getSpriteManager().getAllSprites().size() generateManySpheres(30); } // stop ship from moving forward myShip.applyTheBrakes(event.getX(), event.getY()); // move forward and rotate ship myShip.plotCourse(event.getX(), event.getY(), true); } } }; // Initialize input primaryStage.getScene().setOnMousePressed(fireOrMove); //addEventHandler(MouseEvent.MOUSE_PRESSED, me); // set up stats EventHandler changeWeapons = new EventHandler() { @Override public void handle(KeyEvent event) { myShip.changeWeapon(event.getCode()); } }; primaryStage.getScene().setOnKeyPressed(changeWeapons); // set up stats EventHandler showMouseMove = new EventHandler() { @Override public void handle(MouseEvent event) { mousePtLabel.setText("Mouse PT = (" + event.getX() + ", " + event.getY() + ")"); } }; primaryStage.getScene().setOnMouseMoved(showMouseMove); } /** * Make some more space spheres (Atomic particles) * * @param numSpheres The number of random sized, color, and velocity atoms to generate. */ private void generateManySpheres(int numSpheres) { Random rnd = new Random(); Scene gameSurface = getGameSurface(); for (int i = 0; i < numSpheres; i++) { Color c = Color.rgb(rnd.nextInt(255), rnd.nextInt(255), rnd.nextInt(255)); Atom b = new Atom(rnd.nextInt(15) + 5, c, true); Circle circle = b.getAsCircle(); // random 0 to 2 + (.0 to 1) * random (1 or -1) b.vX = (rnd.nextInt(2) + rnd.nextDouble()) * (rnd.nextBoolean() ? 1 : -1); b.vY = (rnd.nextInt(2) + rnd.nextDouble()) * (rnd.nextBoolean() ? 1 : -1); // random x between 0 to width of scene double newX = rnd.nextInt((int) gameSurface.getWidth()); // check for the right of the width newX is greater than width // minus radius times 2(width of sprite) if (newX > (gameSurface.getWidth() - (circle.getRadius() * 2))) { newX = gameSurface.getWidth() - (circle.getRadius() * 2); } // check for the bottom of screen the height newY is greater than height // minus radius times 2(height of sprite) double newY = rnd.nextInt((int) gameSurface.getHeight()); if (newY > (gameSurface.getHeight() - (circle.getRadius() * 2))) { newY = gameSurface.getHeight() - (circle.getRadius() * 2); } circle.setTranslateX(newX); circle.setTranslateY(newY); circle.setVisible(true); circle.setId(b.toString()); circle.setCache(true); circle.setCacheHint(CacheHint.SPEED); circle.setManaged(false); // add to actors in play (sprite objects) getSpriteManager().addSprites(b); // add sprite's getSceneNodes().getChildren().add(0, b.node); } } /** * Each sprite will update it's velocity and bounce off wall borders. * * @param sprite - An atomic particle (a sphere). */ @Override protected void handleUpdate(Sprite sprite) { // advance object sprite.update(); if (sprite instanceof Missile) { removeMissiles((Missile) sprite); } else { bounceOffWalls(sprite); } } /** * Change the direction of the moving object when it encounters the walls. * * @param sprite The sprite to update based on the wall boundaries. * TODO The ship has got issues. */ private void bounceOffWalls(Sprite sprite) { // bounce off the walls when outside of boundaries Node displayNode; if (sprite instanceof Ship) { displayNode = sprite.node;//((Ship)sprite).getCurrentShipImage(); } else { displayNode = sprite.node; } // Get the group node's X and Y but use the ImageView to obtain the width. if (sprite.node.getTranslateX() > (getGameSurface().getWidth() - displayNode.getBoundsInParent().getWidth()) || displayNode.getTranslateX() < 0) { // bounce the opposite direction sprite.vX = sprite.vX * -1; } // Get the group node's X and Y but use the ImageView to obtain the height. if (sprite.node.getTranslateY() > getGameSurface().getHeight() - displayNode.getBoundsInParent().getHeight() || sprite.node.getTranslateY() < 0) { sprite.vY = sprite.vY * -1; } } /** * Remove missiles when they reach the wall boundaries. * * @param missile The missile to remove based on the wall boundaries. */ private void removeMissiles(Missile missile) { // bounce off the walls when outside of boundaries if (missile.node.getTranslateX() > (getGameSurface().getWidth() - missile.node.getBoundsInParent().getWidth()) || missile.node.getTranslateX() < 0) { getSpriteManager().addSpritesToBeRemoved(missile); getSceneNodes().getChildren().remove(missile.node); } if (missile.node.getTranslateY() > getGameSurface().getHeight() - missile.node.getBoundsInParent().getHeight() || missile.node.getTranslateY() < 0) { getSpriteManager().addSpritesToBeRemoved(missile); getSceneNodes().getChildren().remove(missile.node); } } /** * How to handle the collision of two sprite objects. Stops the particle * by zeroing out the velocity if a collision occurred. * * @param spriteA Sprite from the first list. * @param spriteB Sprite from the second list. * @return boolean returns a true if the two sprites have collided otherwise false. */ @Override protected boolean handleCollision(Sprite spriteA, Sprite spriteB) { if (spriteA != spriteB) { if (spriteA.collide(spriteB)) { if (spriteA instanceof Atom && spriteB instanceof Atom) { ((Atom) spriteA).implode(this); // will remove from the Scene onFinish() ((Atom) spriteB).implode(this); getSpriteManager().addSpritesToBeRemoved(spriteA, spriteB); return true; } } } return false; } }
船
Ship类代表我们很酷的太空飞船。 Ship类继承自Sprite类,以帮助我们包含速度信息(向量)。 此类还将包含一个双向链接列表,该列表包含32个ImageView ( RotatedShipImage )实例,这些实例表示模拟船绕其中心(质心)旋转的每个方向。 在某些时候,我想通过旋转单个SVGPath对象来更改此设置(我知道有一些折衷方案)。 在本教程中,我通过将ImageView的对象从0到360度均匀旋转32方向来实现飞船。 下面的图3中显示的是使用32个ImageView实例和飞船图像的单个Image对象的所有32个方向,以模拟围绕其中心(枢轴点)的旋转。
在给船旋转动画时,我只需使用ImageView节点上的setVisible(true)方法将当前图像以外的所有图像设置为可见。
免责声明 :在游戏中,您不可避免地会遇到数学(三角学)。 如果您有兴趣并想进一步深入,请查看TheExpanse类的initialize()方法的源代码。 在方法的末尾取消注释该语句: getSceneNodes()。getChildren()。add(stats); 。 这将显示控件,使您可以用来调试和检查鼠标按下的坐标。 此外,您还可以在控制台(stdout)中看到与角度,向量等有关的输出。
船舶的成员变量 :
- turnDirection –枚举DIRECTION,顺时针,逆时针,且均不
- u – Vec对象,该对象包含相对于船舶坐标中心的矢量,表示船舶开始旋转的起始方向
- directionalShips – RotatedShipImage对象的列表,每个对象具有对其他RotatedShipImage对象的上一个和下一个引用。 零度(uIndex = 0)是太空飞船朝东的时间。 当旋转JavaFX节点时,逆时针方向为正数,以度为单位
- uIndex –当前RotatedShipImage在directionShips列表中将显示的索引
- vIndex –旋转动画结束时将显示的directionalShips列表中RotatedShipImage的索引
- stopArea –一个JavaFX Circle,其半径为船舶知道何时停止船舶移动
- flipBook –包含所有RotatedShipImage对象的JavaFX组(32)。 该组将在场景上渲染。 就像动画中的翻书一样,每个RotatedShipImage都将根据uIndex和vIndex确定要显示
- keyCode – JavaFX KeyCode将帮助确定是否按键可以帮助您改变武器(字符“ 2”?)
该船的会员职能 :
- update() –更新船只的速度和方向。 还将确定何时停止移动。
- getCurrentShipImage() –基于uIndex,它返回ImageView,该ImageView是正在显示的当前船方向图像
- getCenterX() –返回屏幕中心的X坐标
- getCenterY() –返回屏幕上船中心的X坐标
- plotCourse(double screenX,double screenY,boolean推力) –用户在屏幕上单击鼠标后,此方法将计算旋转船的角度并更改速度以将坐标推向目标点。 使用Vec对象时,屏幕坐标将转换为直角坐标,以确定两个向量(U和V)之间的角度。
- turnShip() – plotCourse()方法调用turnShip()方法来执行船舶旋转的实际动画
- applyTheBrakes(double screenX,double screenY) –用户选择(右键单击)船只将导航至的位置applyTheBrakes()方法只需设置stopArea ( Circle )对象即可让船只知道何时停止
- fire() –返回供游戏引擎放入场景中的导弹(Sprite)对象。 每个导弹都以增加的速度(增加的速度)包含与船相同的方向。 应该比飞船飞得更快。
- changeWeapon(KeyCode keyCode) –用户(玩家)击键为'2? 武器将改变以产生更大的导弹射弹,但速度稍慢。 其他任何按键操作都将是默认的武器,它可以产生速度更快的小型导弹弹丸。
下面显示的是类图的图4,显示了Ship类的成员。
船级图
下面显示的是Ship类的源代码。
package carlfx.demos.navigateship; import carlfx.gameengine.Sprite; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.animation.TimelineBuilder; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.CacheHint; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.util.Duration; import java.util.ArrayList; import java.util.List; /** * A space ship with 32 directions * When two atoms collide each will fade and become removed from the scene. The * method called implode() implements a fade transition effect. * * @author cdea */ public class Ship extends Sprite { /** * 360 degree turn */ private final static int TWO_PI_DEGREES = 360; /** * Number of ship frames and directions the ship is pointing nose */ private final static int NUM_DIRECTIONS = 32; /** * The angle of one direction (adjacent directions) (11.25 degrees) */ private final static float UNIT_ANGLE_PER_FRAME = ((float) TWO_PI_DEGREES / NUM_DIRECTIONS); /** * Amount of time it takes the ship to move 180 degrees in milliseconds. */ private final static int MILLIS_TURN_SHIP_180_DEGREES = 300; /** * When the ship turns on each direction one amount of time for one frame or turn of the ship. (18.75 milliseconds) */ private final static float MILLIS_PER_FRAME = (float) MILLIS_TURN_SHIP_180_DEGREES / (NUM_DIRECTIONS / 2); /** * All possible turn directions Clockwise, Counter Clockwise, or Neither when the user clicks mouse around ship */ private enum DIRECTION { CLOCKWISE, COUNTER_CLOCKWISE, NEITHER } /** * Velocity amount used vector when ship moves forward. scale vector of ship. See flipBook translateX and Y. */ private final static float THRUST_AMOUNT = 3.3f; /***/ private final static float MISSILE_THRUST_AMOUNT = 6.3F; /** * Angle in degrees to rotate ship. */ /** * Current turning direction. default is NEITHER. Clockwise and Counter Clockwise. */ private DIRECTION turnDirection = DIRECTION.NEITHER; /** * The current starting position of the vector or coordinate where the nose of the ship is pointing towards. */ private Vec u; // current or start vector /** * All ImageViews of all the possible image frames for each direction the ship is pointing. ie: 32 directions. */ private final List directionalShips = new ArrayList<>(); /** * The Timeline instance to animate the ship rotating using images. This is an optical illusion similar to page * flipping as each frame is displayed the previous visible attribute is set to false. No rotation is happening. */ private Timeline rotateShipTimeline; /** * The current index into the list of ImageViews representing each direction of the ship. Zero is the ship * pointing to the right or zero degrees. */ private int uIndex = 0; /** * The end index into the list of ImageViews representing each direction of the ship. Zero is the ship * pointing to the right or zero degrees. */ private int vIndex = 0; /** * The spot where the user has right clicked letting the engine check the ship's center is in this area. */ private final Circle stopArea = new Circle(); /** * A group contain all of the ship image view nodes. */ private final Group flipBook = new Group(); /** * A key code will be used for weapon selection. */ private KeyCode keyCode; public Ship() { // Load one image. Image shipImage = new Image(getClass().getClassLoader().getResource("ship.png").toExternalForm(), true); stopArea.setRadius(40); RotatedShipImage prev = null; // create all the number of directions based on a unit angle. 360 divided by NUM_DIRECTIONS for (int i = 0; i < NUM_DIRECTIONS; i++) { RotatedShipImage imageView = new RotatedShipImage(); imageView.setImage(shipImage); imageView.setRotate(-1 * i * UNIT_ANGLE_PER_FRAME); imageView.setCache(true); imageView.setCacheHint(CacheHint.SPEED); imageView.setManaged(false); imageView.prev = prev; imageView.setVisible(false); directionalShips.add(imageView); if (prev != null) { prev.next = imageView; } prev = imageView; flipBook.getChildren().add(imageView); } RotatedShipImage firstShip = directionalShips.get(0); firstShip.prev = prev; prev.next = firstShip; // set javafx node to an image firstShip.setVisible(true); node = flipBook; flipBook.setTranslateX(200); flipBook.setTranslateY(300); } /** * Change the velocity of the atom particle. */ @Override public void update() { flipBook.setTranslateX(flipBook.getTranslateX() + vX); flipBook.setTranslateY(flipBook.getTranslateY() + vY); if (stopArea.contains(getCenterX(), getCenterY())) { vX = 0; vY = 0; } } private RotatedShipImage getCurrentShipImage() { return directionalShips.get(uIndex); } /** * The center X coordinate of the current visible image. See <code>getCurrentShipImage()</code> method. * * @return The scene or screen X coordinate. */ public double getCenterX() { RotatedShipImage shipImage = getCurrentShipImage(); return node.getTranslateX() + (shipImage.getBoundsInLocal().getWidth() / 2); } /** * The center Y coordinate of the current visible image. See <code>getCurrentShipImage()</code> method. * * @return The scene or screen Y coordinate. */ public double getCenterY() { RotatedShipImage shipImage = getCurrentShipImage(); return node.getTranslateY() + (shipImage.getBoundsInLocal().getHeight() / 2); } /** * Determines the angle between it's starting position and ending position (Similar to a clock's second hand). * When the user is shooting the ship nose will point in the direction of the mouse press using the primary button. * When the user is thrusting to a location on the screen the right click mouse will pass true to the thrust * parameter. * * @param screenX The mouse press' screen x coordinate. * @param screenY The mouse press' screen ycoordinate. * @param thrust Thrust ship forward or not. True move forward otherwise false. */ public void plotCourse(double screenX, double screenY, boolean thrust) { // get center of ship double sx = getCenterX(); double sy = getCenterY(); // get user's new turn position based on mouse click Vec v = new Vec(screenX, screenY, sx, sy); if (u == null) { u = new Vec(1, 0); } double atan2RadiansU = Math.atan2(u.y, u.x); double atan2DegreesU = Math.toDegrees(atan2RadiansU); double atan2RadiansV = Math.atan2(v.y, v.x); double atan2DegreesV = Math.toDegrees(atan2RadiansV); double angleBetweenUAndV = atan2DegreesV - atan2DegreesU; // if abs value is greater than 180 move counter clockwise //(or opposite of what is determined) double absAngleBetweenUAndV = Math.abs(angleBetweenUAndV); boolean goOtherWay = false; if (absAngleBetweenUAndV > 180) { if (angleBetweenUAndV < 0) { turnDirection = DIRECTION.COUNTER_CLOCKWISE; goOtherWay = true; } else if (angleBetweenUAndV > 0) { turnDirection = DIRECTION.CLOCKWISE; goOtherWay = true; } else { turnDirection = Ship.DIRECTION.NEITHER; } } else { if (angleBetweenUAndV < 0) { turnDirection = Ship.DIRECTION.CLOCKWISE; } else if (angleBetweenUAndV > 0) { turnDirection = Ship.DIRECTION.COUNTER_CLOCKWISE; } else { turnDirection = Ship.DIRECTION.NEITHER; } } double degreesToMove = absAngleBetweenUAndV; if (goOtherWay) { degreesToMove = TWO_PI_DEGREES - absAngleBetweenUAndV; } //int q = v.quadrant(); uIndex = Math.round((float) (atan2DegreesU / UNIT_ANGLE_PER_FRAME)); if (uIndex < 0) { uIndex = NUM_DIRECTIONS + uIndex; } vIndex = Math.round((float) (atan2DegreesV / UNIT_ANGLE_PER_FRAME)); if (vIndex < 0) { vIndex = NUM_DIRECTIONS + vIndex; } String debugMsg = turnDirection + " U [m(" + u.mx + ", " + u.my + ") => c(" + u.x + ", " + u.y + ")] " + " V [m(" + v.mx + ", " + v.my + ") => c(" + v.x + ", " + v.y + ")] " + " start angle: " + atan2DegreesU + " end angle:" + atan2DegreesV + " Angle between: " + degreesToMove + " Start index: " + uIndex + " End index: " + vIndex; System.out.println(debugMsg); if (thrust) { vX = Math.cos(atan2RadiansV) * THRUST_AMOUNT; vY = -Math.sin(atan2RadiansV) * THRUST_AMOUNT; } turnShip(); u = v; } private void turnShip() { final Duration oneFrameAmt = Duration.millis(MILLIS_PER_FRAME); RotatedShipImage startImage = directionalShips.get(uIndex); RotatedShipImage endImage = directionalShips.get(vIndex); List frames = new ArrayList<>(); RotatedShipImage currImage = startImage; int i = 1; while (true) { final Node displayNode = currImage; KeyFrame oneFrame = new KeyFrame(oneFrameAmt.multiply(i), new EventHandler() { @Override public void handle(javafx.event.ActionEvent event) { // make all ship images invisible for (RotatedShipImage shipImg : directionalShips) { shipImg.setVisible(false); } // make current ship image visible displayNode.setVisible(true); // update the current index //uIndex = directionalShips.indexOf(displayNode); } }); // oneFrame frames.add(oneFrame); if (currImage == endImage) { break; } if (turnDirection == DIRECTION.CLOCKWISE) { currImage = currImage.prev; } if (turnDirection == DIRECTION.COUNTER_CLOCKWISE) { currImage = currImage.next; } i++; } if (rotateShipTimeline != null) { rotateShipTimeline.stop(); rotateShipTimeline.getKeyFrames().clear(); rotateShipTimeline.getKeyFrames().addAll(frames); } else { // sets the game world's game loop (Timeline) rotateShipTimeline = TimelineBuilder.create() .keyFrames(frames) .build(); } rotateShipTimeline.playFromStart(); } /** * Stops the ship from thrusting forward. * * @param screenX the screen's X coordinate to stop the ship. * @param screenY the screen's Y coordinate to stop the ship. */ public void applyTheBrakes(double screenX, double screenY) { stopArea.setCenterX(screenX); stopArea.setCenterY(screenY); } public Missile fire() { Missile m1; float slowDownAmt = 0; if (KeyCode.DIGIT2 == keyCode) { m1 = new Missile(10, Color.BLUE); slowDownAmt = 2.3f; } else { m1 = new Missile(Color.RED); } // velocity vector of the missile m1.vX = Math.cos(Math.toRadians(uIndex * UNIT_ANGLE_PER_FRAME)) * (MISSILE_THRUST_AMOUNT - slowDownAmt); m1.vY = -Math.sin(Math.toRadians(uIndex * UNIT_ANGLE_PER_FRAME)) * (MISSILE_THRUST_AMOUNT - slowDownAmt); // make the missile launch in the direction of the current direction of the ship nose. based on the // current frame (uIndex) into the list of image view nodes. RotatedShipImage shipImage = directionalShips.get(uIndex); // start to appear in the center of the ship to come out the direction of the nose of the ship. double offsetX = (shipImage.getBoundsInLocal().getWidth() - m1.node.getBoundsInLocal().getWidth()) / 2; double offsetY = (shipImage.getBoundsInLocal().getHeight() - m1.node.getBoundsInLocal().getHeight()) / 2; // initial launch of the missile m1.node.setTranslateX(node.getTranslateX() + offsetX + m1.vX); m1.node.setTranslateY(node.getTranslateY() + offsetY + m1.vY); return m1; } public void changeWeapon(KeyCode keyCode) { this.keyCode = keyCode; } }
Vec
Vec类是一个简单的帮助程序容器类,可帮助保持鼠标单击的屏幕坐标,并根据子画面,图像或形状的中心将其转换为笛卡尔坐标。 此类用于帮助确定两个向量[Math.atan2(y,x)]之间的角度。 通过确定角度,船可以执行子画面图像的旋转动画。
下面显示的是Vec类的源代码。
package carlfx.demos.navigateship; /** * This class represents a container class to hold a Vector in space and direction * the ship will move. Assuming the center of the ship is the origin the angles can * be determined by a unit circle via Cartesian coordinates. * When the user clicks on the screen the mouse coordinates or screen coordinates * will be stored into the mx and my instance variables. * The x and y data members are converted to cartesian coordinates before storing. * * I purposefully left out getters and setters. In gaming just keep things minimalistic. * @author cdea */ public class Vec { public double mx; public double my; public double x; public double y; /** * This is a default constructor which will take a Cartesian coordinate. * @param x X coordinate of a point on a Cartesian system. * @param y Y coordinate of a point on a Cartesian system. */ public Vec(float x, float y) { this.x = x; this.y = y; } /** * Constructor will convert mouse click points into Cartesian coordinates based on the sprite's center point as * the origin. * @param mx Mouse press' screen X coordinate. * @param my Mouse press' screen Y coordinate. * @param centerX Screen X coordinate of the center of the ship sprite. * @param centerY Screen Y coordinate of the center of the ship sprite. */ public Vec(double mx, double my, double centerX, double centerY) { this.x = convertX(mx, centerX); this.y = convertY(my, centerY); this.mx = mx; this.my = my; } /** * Returns a Cartesian coordinate system's quadrant from 1 to 4. * * first quadrant - 1 upper right * second quadrant - 2 upper left * third quadrant - 3 lower left * fourth quadrant - 4 lower right * * @return int quadrant number 1 through 4 */ public int quadrant() { int q = 0; if (x > 0 && y > 0) { q =1; } else if (x < 0 && y > 0) { q = 2; } else if (x < 0 && y < 0) { q = 3; } else if (x > 0 && y < 0) { q = 4; } return q; } @Override public String toString(){ return "(" + x + "," + y + ") quadrant=" + quadrant(); } /** * Converts point's X screen coordinate into a Cartesian system. * @param mouseX Converts the mouse X coordinate into Cartesian system based on the ship center X (originX). * @param originX The ship center point's X coordinate. * @return double value of a Cartesian system X coordinate based on the origin X. */ static double convertX(double mouseX, double originX) { return mouseX - originX; } /** * Converts point's Y screen coordinate into a Cartesian system. * @param mouseY Converts the mouse Y coordinate into Cartesian system based on the ship center Y (originY). * @param originY The ship center point's Y coordinate. * @return double value of a Cartesian system Y coordinate based on the origin Y. */ static double convertY(double mouseY, double originY) { return originY - mouseY; } }
RotatedShipImage
RotatedShipImage类继承自JavaFX的ImageView类,但还包含对上一个和下一个RotatedShipImage实例的引用, 这些实例构成了一个双向链接列表。 图3描绘了在每个RotatedShipImage中呈现的“ ship.png”的32个图像,它们全部放置在JavaFX Group节点中。 当船似乎在旋转时,一次只显示一幅图像。
下面显示的是RotatedShipImage类的源代码。
package carlfx.demos.navigateship; import javafx.scene.image.ImageView; /** * Represents a double link list to assist in the rotation of the ship. * This helps to move clockwise and counter clockwise. */ public class RotatedShipImage extends ImageView { public RotatedShipImage next; public RotatedShipImage prev; }
导弹
Missile类继承自Atom类。 导弹是标记物,用于区分小球和导弹。 制造导弹后,它们将以更大的速度包含与船相同的方向(船鼻指向的方向)。
下面显示的是Missile类的源代码。
package carlfx.demos.navigateship; import javafx.scene.paint.Color; /** * A missile projectile without the radial gradient. */ public class Missile extends Atom { public Missile(Color fill) { super(5, fill, false); } public Missile(int radius, Color fill) { super(radius, fill, true); } }
结论
输入对任何游戏都至关重要,因此通常很难正确输入。 较旧的游戏引擎将在游戏循环中进行轮询。 使用JavaFX 2.x的事件处理时,可以实现要添加到场景图或单个Node对象中的事件类型。 希望在将来,我们将看到更多用于游戏的巧妙输入设备(请参阅Oracle的Java技术推广员Simon Ritter )。 睁大眼睛看一下第4部分,它涉及碰撞检测。 因此,请继续关注并随时发表评论。
有用的链接:
7-11 :http://www.7-eleven.com
玩小行星 :http://www.play.vg/games/4-Asteroids.html
小行星 :http://en.wikipedia.org/wiki/Asteroids_(video_game)
斯科特·萨夫兰(Scott Safran) :http://en.wikipedia.org/wiki/Scott_Safran
后院拱廊 :http://www.themysteryworld.com/2011/02/guy-builds-video-arcade-in-his-back.html
缩小《星战》 :http://techland.time.com/2012/04/26/man-builds-16-scale-star-wars-arcade-game/
三角学 :http://en.wikipedia.org/wiki/ 三角学
JavaFX节点API :http://docs.oracle.com/javafx/2/api/javafx/scene/Node.html
JavaFX Scene API :http://docs.oracle.com/javafx/2/api/javafx/scene/Scene.html
JavaFX SVGPath API :http: //docs.oracle.com/javafx/2/api/javafx/scene/shape/SVGPath.html
多点触控和手势支持 :http://www.oracle.com/technetwork/java/javafx/overview/roadmap-1446331.html
Pro JavaFX 2 Apress发布– pg。 62第2章第2节“处理输入事件”。 http://www.apress.com/9781430268727
Java 7 Recipe Apress发行-第pg。 602第16章食谱16-3“沿路径动画制作形状”。 http://www.apress.com/9781430240563
电子游戏厅柜 :http://en.wikipedia.org/wiki/Video_game_arcade_cabinet
栅格图形 :http://en.wikipedia.org/wiki/Raster_graphics
GitHub上的第3部分源代码 :https://github.com/carldea/JFXGen/tree/master/demos/navigateship
JavaFX Canvas节点 :http://mail.openjdk.java.net/pipermail/openjfx-dev/2012-April/001210.html
JavaFX-为JavaFX应用程序优化性能 :http://www.parleys.com/#st=5&id=2738&sl=0
Ritter :https://blogs.oracle.com/javaone/entry/interface_with_the_interface_javafx
视频游戏高中第1集 :http://www.rocketjump.com/?video = vghs-episode-1
视频游戏高中第2集 :http://www.rocketjump.com/?video = vghs-episode-2-5
参考:来自我们的JCG合作伙伴 Carl Dea的JavaFX 2 GameTutorial第3部分 ,位于Carl's FX Blog博客上。
翻译自: https://www.javacodegeeks.com/2012/05/javafx-2-gametutorial-part-3.html