本节书摘来异步社区《Java 2D游戏编程入门》一书中的第8章,第8.1节,作者:【美】Timothy Wright(莱特),更多章节内容可以访问云栖社区“异步社区”公众号查看。
制作一款2D太空飞船游戏时,首先要解决的大问题是,当飞船到达屏幕边缘时,会发生什么事情。一个解决方案是,将飞船保持在屏幕的中央,而移动其周围的环境。没有对加载文件的任何支持,也没有关卡编辑器的话,要做到这点似乎有点难。另一个选择是,当飞船到达屏幕边界的时候,将其弹回。这样做似乎显得很奇怪。第三种选择是,让飞船从一端到另一端折返。让我们采用第三种方法。
还有很多方法可以解决这一问题。一种方法是,让飞船完全离开屏幕,然后让其在另一端返回,如图8.1所示。
尝试这一想法似乎很简单。我们使用围绕飞船的一个边界框来测试飞船何时离开了屏幕,并且将其移动到另外一端。这种方法有两个问题:一个问题是,可能会使得飞船在飞行中变成只有很小的一块可见,从而很难碰到它;第二个问题是,飞船会持续地处在屏幕之外。这并不经常发生,但是,当多边形的大部分都在屏幕之外的时候,可能很难撞击到该物体。由于存在这些问题,因此与一直等待直到整个物体离开屏幕相比,折返物体似乎是更好的选择。
折返物体以使得离开屏幕一端的任何部分从屏幕的另一端返回,也需要担心风险。当你将一个物体从一端折返到另一端的时候,同一物体可能会有4个副本,如图8.2所示。
这使得不仅需要绘制4个副本,而且所有4个副本都需要进行碰撞检查。如果只对第一个物体进行碰撞检查,位于其对角的两个物体可能就不会记录碰撞,即便它们的绘制物体有重叠,如图8.3所示。
创建一个渲染列表,其中包含了每个物体的副本,这可以解决该问题。物体可以绘制,并且整个列表可用来检查碰撞。物体折返的时候不会遇到问题,因为一旦物体的中心到了游戏世界的边界之外,就会通过游戏世界的宽度和高度来调整其位置以将其折返。应该总是将折返位置放置在边界之中,这样做物体就不会跑到屏幕之外,如图8.4所示。
if( p < min ) p += w; if( p > max ) p −= w;``` 一旦物体的位置折返了,通过计算最小和最大(x,y)值来得到物体的 AABB,如图8.5所示。 <div style="text-align: center"><img src="https://yqfile.alicdn.com/413b80946f83687296413d8461e78fb715e0c9c6.png" width="" height=""> </div> 边界框判断物体是否在屏幕的边界之外,以及是否需要将其折返到另一端。 位于javagames.prototype包中的Polygon Wrapper类,包含了折返多边形及位置的方法。构造方法接受世界坐标表示的屏幕的宽度和高度,并且计算最小和最大向量值。hasLeftWorld()方法根据最小和最大世界坐标来检查给定的位置。如果给定的位置在世界的边界之外,wrapPosition()方法返回一个折返位置。通过世界的宽度和高度来调整该折返位置,以便新的位置不会在世界的边界之外。要折返的多边形的边界,通过如下方式计算:// PolygonWrapper.javaprivate Vector2f getMin( Vector2f[] poly ) { Vector2f min = new Vector2f( Float.MAX_VALUE, Float.MAX_VALUE ); for( Vector2f v : poly ) { min.x = Math.min( v.x, min.x ); min.y = Math.min( v.y, min.y ); } return min;}private Vector2f getMax( Vector2f[] poly ) { Vector2f max = new Vector2f( -Float.MAX_VALUE, -Float.MAX_VALUE ); for( Vector2f v : poly ) { max.x = Math.max( v.x, max.x ); max.y = Math.max( v.y, max.y ); } return max;}`wrapPolygon()方法计算最小和最大值,并且使用这些值来判断物体是否需要向北、南、东或西折返。如果物体向两个主要方向折返,例如,向南和向东,那么,它也需要向北和向西折返。四个主要方向的每一个都要进行测试。使用变换方法,复制每个折返多边形,并且将其添加到渲染列表:
// PolygonWrapper.java private Vector2f[] transform( Vector2f[] poly, Matrix3x3f mat ) { Vector2f[] copy = new Vector2f[ poly.length ]; for( int i = 0; i < poly.length; ++i ) { copy[i] = mat.mul( poly[i] ); } return copy; }``` 通过加上或减去世界的宽度和高度,在8个方向上折返多边形,如图8.6所示。 <div style="text-align: center"><img src="https://yqfile.alicdn.com/66b94eb9fb79fc2c0e599a61c0acd5e87016b0c7.png" width="" height=""> </div> PolygonWrapper代码如下所示:package javagames.prototype;import java.util.List;import javagames.util.Matrix3x3f;import javagames.util.Vector2f;
public class PolygonWrapper { private float worldWidth; private float worldHeight; private Vector2f worldMin; private Vector2f worldMax; public PolygonWrapper( float worldWidth, float worldHeight ) { this.worldWidth = worldWidth; this.worldHeight = worldHeight; worldMax = new Vector2f( worldWidth / 2.0f, worldHeight / 2.0f ); worldMin = worldMax.inv(); } public boolean hasLeftWorld( Vector2f position ) { return position.x < worldMin.x || position.x > worldMax.x || position.y < worldMin.y || position.y > worldMax.y; } public Vector2f wrapPosition( Vector2f position ) { Vector2f wrapped = new Vector2f( position ); if( position.x < worldMin.x ) { wrapped.x = position.x + worldWidth; } else if( position.x > worldMax.x ) { wrapped.x = position.x - worldWidth; } if( position.y < worldMin.y ) { wrapped.y = position.y + worldHeight; } else if( position.y > worldMax.y ) { wrapped.y = position.y - worldHeight; } return wrapped; } public void wrapPolygon( Vector2f[] poly, List renderList ) { Vector2f min = getMin( poly ); Vector2f max = getMax( poly ); boolean north = max.y > worldMax.y; boolean south = min.y < worldMin.y; boolean west = min.x < worldMin.x; boolean east = max.x > worldMax.x; if( west ) renderList.add( wrapEast( poly ) ); if( east ) renderList.add( wrapWest( poly ) ); if( north ) renderList.add( wrapSouth( poly ) ); if( south ) renderList.add( wrapNorth( poly ) ); if( north && west ) renderList.add( wrapSouthEast( poly ) ); if( north && east ) renderList.add( wrapSouthWest( poly ) ); if( south && west ) renderList.add( wrapNorthEast( poly ) ); if( south && east ) renderList.add( wrapNorthWest( poly ) ); } private Vector2f getMin( Vector2f[] poly ) { Vector2f min = new Vector2f( Float.MAX_VALUE, Float.MAX_VALUE ); for( Vector2f v : poly ) { min.x = Math.min( v.x, min.x ); min.y = Math.min( v.y, min.y ); } return min; } private Vector2f getMax( Vector2f[] poly ) { Vector2f max = new Vector2f( -Float.MAX_VALUE, -Float.MAX_VALUE ); for( Vector2f v : poly ) { max.x = Math.max( v.x, max.x ); max.y = Math.max( v.y, max.y ); } return max; } private Vector2f[] wrapNorth( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( 0.0f, worldHeight ) ); } private Vector2f[] wrapSouth( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( 0.0f, -worldHeight ) ); } private Vector2f[] wrapEast( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( worldWidth, 0.0f ) ); } private Vector2f[] wrapWest( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( -worldWidth, 0.0f ) ); } private Vector2f[] wrapNorthWest( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( -worldWidth, worldHeight ) ); } private Vector2f[] wrapNorthEast( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( worldWidth, worldHeight ) ); } private Vector2f[] wrapSouthEast( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( worldWidth, -worldHeight ) ); } private Vector2f[] wrapSouthWest( Vector2f[] poly ) { return transform( poly, Matrix3x3f.translate( -worldWidth, -worldHeight ) ); } private Vector2f[] transform( Vector2f[] poly, Matrix3x3f mat ) { Vector2f[] copy = new Vector2f[ poly.length ]; for( int i = 0; i < poly.length; ++i ) { copy[i] = mat.mul( poly[i] ); } return copy; }}`位于javagames.prototype包中的ScreenWrapExample,如图8.7所示,使用PolygonWrapper类来折返一个屏幕上来回移动的方块。pos向量保存了方块的位置。poly数组保存了方块多边形。renderList保存了要折返的方块的副本,以及最初的变换多边形。折返变量是前面所介绍的PolygonWrapper。
initialize()方法创建了所有的物体并且将鼠标设置为相对移动,从而不需要鼠标离开屏幕物体就可以在屏幕上折返。如果使用绝对位置的话,processInput()方法使用鼠标位置来移动方块;如果使用相对移动的话,它通过将当前位置和相对移动相加来移动方块。该方法还使用空格键来切换相对鼠标移动。
transform()方法创建了对象的一个副本,并且通过给定的矩形来变换它。updateObjects()方法清理了渲染列表,将方块折返并变换,然后使用PolygonWrapper将物体的任何副本添加到渲染列表中。render()方法将渲染列表中的所有物体绘制到屏幕上。
package javagames.prototype; import java.awt.*; import java.awt.event.KeyEvent; import java.util.ArrayList; import javagames.util.*; public class ScreenWrapExample extends SimpleFramework { private Vector2f pos; private Vector2f[] poly; private ArrayList<Vector2f[]> renderList; private PolygonWrapper wrapper; public ScreenWrapExample() { appBorderScale = 0.9f; appWidth = 640; appHeight = 640; appMaintainRatio = true; appTitle = "Screen Wrap Example"; appBackground = Color.WHITE; appFPSColor = Color.BLACK; } @Override protected void initialize() { super.initialize(); mouse.setRelative( true ); renderList = new ArrayList<Vector2f[]>(); wrapper = new PolygonWrapper( appWorldWidth, appWorldHeight ); poly = new Vector2f[] { new Vector2f( -0.125f, 0.125f ), new Vector2f( 0.125f, 0.125f ), new Vector2f( 0.125f, -0.125f ), new Vector2f( -0.125f, -0.125f ), }; pos = new Vector2f(); } @Override protected void processInput( float delta ) { super.processInput( delta ); if( mouse.isRelative() ) { Vector2f v = getRelativeWorldMousePosition(); pos = pos.add( v ); } else { pos = getWorldMousePosition(); } if( keyboard.keyDownOnce( KeyEvent.VK_SPACE ) ) { mouse.setRelative( !mouse.isRelative() ); if( mouse.isRelative() ) { pos = new Vector2f(); } } } @Override protected void updateObjects( float delta ) { super.updateObjects( delta ); renderList.clear(); pos = wrapper.wrapPosition( pos ); Vector2f[] world = transform( poly, Matrix3x3f.translate( pos ) ); renderList.add( world ); wrapper.wrapPolygon( world, renderList ); } private Vector2f[] transform( Vector2f[] poly, Matrix3x3f mat ) { Vector2f[] copy = new Vector2f[ poly.length ]; for( int i = 0; i < poly.length; ++i ) { copy[i] = mat.mul( poly[i] ); } return copy; } @Override protected void render( Graphics g ) { super.render( g ); g.drawString( "Press Space to Toggle mouse", 20, 35 ); Matrix3x3f view = getViewportTransform(); for( Vector2f[] toRender : renderList ) { for( int i = 0; i < toRender.length; ++i ) { toRender[i] = view.mul( toRender[i] ); } Utility.drawPolygon( g, toRender ); } } public static void main( String[] args ) { launchApp( new ScreenWrapExample() ); } 相关资源:WPF编程宝典 part1