《Java 2D游戏编程入门》—— 8.1 创建一个多边形包装类

8.1 创建一个多边形包装类

制作一款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.java
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;
}`
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() );
  }
时间: 2024-09-21 16:32:12

《Java 2D游戏编程入门》—— 8.1 创建一个多边形包装类的相关文章

《Java 2D游戏编程入门》—— 导读

前言 多年前,当我第一次将软件开发作为专业工作的时候,有人请我编写一个applet.那时候,我对于Java语言知道得并不多.在整个上学期间,我很广泛地使用C++.我确实用Java编写过一些代码,但认为它太慢并且是C++的没落版. 同时,我购买和阅读了很多可以接触到的游戏编程图书.我通读了一本关于人工智能的书,其中包含很多不错的示例,但它们都是用C++和DirectX编写的.由于忙着学习Java以便在工作中使用,我认为将示例转换为由Java编写可能是学习这门语言的一种好办法. 毕竟,Java游戏编

《Java 2D游戏编程入门》—— 8.2 创建一个原型小行星

8.2 创建一个原型小行星 PrototypeAsteroid类位于javagames.prototype包中,它表示一个穿越太空的陨石.在创建的时候,使用了一个随机的速率和旋转.Java的随机数生成器只能返回0到1之间的浮点数,因此,要创建在任意范围内分布的随机数,需要一些额外的步骤.例如,要返回-3到7之间的随机数,应按照如下步骤进行. 1.用最大值减去最小值,计算随机数之间的差距. 2.生成从0到1的一个随机浮点数. 3.将随机数乘以差距值. 4.通过加上最小值来迁移范围. 这些步骤听起来

《Java 2D游戏编程入门》—— 8.3 创建一个原型编辑器

8.3 创建一个原型编辑器 现在是时候创建一些多边形了.尽管有可能可以猜到每个点,但要创建9个小行星形状,手动进行的话,工作量还是很大的.就像大多数程序一样,我也很懒.编写一个编辑器来创建多边形,这样会容易很多.位于javagames.prototype包中的PrototypeEditor,如图8.9所示. 如果模型存储为文件,它们将在运行时加载.然而,做这些事情所需的代码还没有介绍过,因此,编辑器将会作弊.当按下空格键的时候,编辑器会产生出能够粘贴到其他源文件中的代码.本书的第二部分将会介绍使

《Java 2D游戏编程入门》—— 1.6 修改显示模式

1.6 修改显示模式 要创建全屏的应用程序,需要修改显示模式.DisplayModeTest如图1.4所示,它使用了Swing组件.这个示例也位于javagames.render包中.如果你还没有编写过任何的Swing程序,一些代码可能会看上去很陌生.有很多相关的图书和教程可供参考,并且如果要详细介绍Swing所提供的所有功能,需要比这本书更大的篇幅. 这个示例应用程序列出了所有可用的屏幕分辨率,这允许用户在全屏模式之间来回切换,以使用任何可用的显示模式.这个示例不仅展示了使用Swing组件编程

《Java 2D游戏编程入门》—— 8.7 编写原型游戏

8.7 编写原型游戏 原型游戏如图8.12所示,位于javagames.prototype包中,它使用了我们目前为止所见过的所有技术.尽管这只是一个原型,并且目前还没有成为一款完整的游戏,但我已经展示了足够的工具来让一些功能奏效.如果要等到最后再制作一款游戏,可能需要等太长的时间. 该原型游戏使用了我们在本章前面所介绍的如下的类. PolygonWrapper PrototypeShip PrototypeAsteroid PrototypeAsteroidFactory PrototypeBu

《Java 2D游戏编程入门》—— 第8章 游戏原型

第8章 游戏原型 本章基于你在前面各章中已经学到的所有内容,介绍如何创建一个原型太空游戏.尽管它还有很多元素缺失,因而不能称之为一款完整的游戏,但我们已经有了足够的可用工具来创建一个可以工作的原型. 在初次学习编程的时候,我遇到的一个问题是,有众多的示例使用各种编程概念,如循环.变量.集成和多态,但是,并没有太多的示例真正做某件事情.这些示例要么太简单了,只是无用的代码片段,要么是极其复杂.编写糟糕的程序,其中还掺入了所有的计算机科学理论,而不是只用到完成任务所需的那些知识.本章的这款原型游戏并

《Java 2D游戏编程入门》—— 1.3 使用主动渲染

1.3 使用主动渲染 前面的示例使用了一种叫做被动渲染(passive rendering)的技术.该应用程序在paint()方法中重新绘制自身,但是,由Swing库来决定什么时候调用该方法.事件分派线程处理Swing组件的渲染,而这并不由你来控制.尽管这对于常规应用程序来说很好,但对于游戏不推荐这么做. 要处理渲染,可以使用BufferStrategy类,但这需要对应用程序的结构做一些修改.这将允许应用程序把渲染代码放在一个单独的线程中,并且由整个进程来控制. 把游戏代码放在paint()方法

《Java 2D游戏编程入门》—— 1.4 创建定制的渲染线程

1.4 创建定制的渲染线程 除了使用前面的代码来执行定制的渲染外,还需要一个定制的渲染线程.为了只是关注线程问题,位于javagames.render包中的RenderThreadExample实际上不会在屏幕上绘制任何内容,它只是在渲染代码应该出现的地方打印出"Game Loop".下面的示例将RenderThreadExample和前面的代码组合起来,创建了一个主动渲染的应用程序. 在RenderThreadExample中,首先需要注意的是,它实现了一个可运行的接口.这个可运行的

《Java 2D游戏编程入门》—— 1.2 创建Hello World应用程序

1.2 创建Hello World应用程序 图1.1所示的Hello World应用程序是第一个游戏窗口的示例.HelloWorldApp位于javagames.render包中.除了清除和重新绘制背景,这个窗口中不再渲染其他内容.HelloWorldApp扩展了JFrame类,这是Java的Swing库中一个顶级的窗口组件.这个应用程序包含一个FrameRate对象,该对象用来测量应用程序的帧速率. 由于Swing库不是线程安全的,因此你应该总是在Swing事件线程上创建并展示一个JFrame