10.2.3 扩展 Actor2D类
为了创建一个生动逼真的Actor2D演示程序,需要创建3个类:定义角色行为的Actor2D类的子类,加载图像帧并定义动画样式的ActorGroup2D对象,把所有一切集合到一起并驱动主事件循环的Applet类。所以,经过短暂的停留后,下面来让一个用户控制的机器人动画动起来。
1.Robot类
这个Robot类继承自Actor2D类,它定义了一个可以往东,南,西,北4个方向走的机器人,在朝这4个方向走时,它还可以用它的武器开火。可以让主Applet类来定义并处理控制机制。在这个类中可以只定义机器人可接受的行为。
检查下面的Robot类的代码,如果对它感到迷惑的话,可以回头参考它的父类的源代码搞清来龙去脉。
import java.awt.*;
//创建一个可以按照指定的方向移动并可以使用武器开火的简单机器人
public class Robot extends Actor2D{
//和当前的动画关联的索引
protected int currAnimIndex;
//为设计动画保存前面的动画
protected int prevAnimIndex;
//Robot的开火状态
public final static int SHOOTING=8;
//用来告诉机器人朝哪个方向移动
public final static int DIR_NORTH=0;
public final static int DIR_SOUTH=1;
public final static int DIR_EAST=2;
public final static int DIR_WEST=3;
//用给定的角色组创建一个新的Robot
public Robot(ActorGroup2D grp){
super(grp);
vel.setX(5);
vel.setY(5);
animWait=3;
currAnimIndex=0;
prevAnimIndex=0;
currAnimation=group.getAnimationStrip(RobotGroup.WALKING_SOUTH);
frameWidth=currAnimation.getFrameWidth();
frameHeight=currAnimation.getFrameHeight();
}
public void update(){
if(hasState(SHOOTING)){
animate();
}
xform.setToTranslation(pos.getX(),pos.getY());
updateBounds();
checkBounds();
}
//让机器人射击直到stopShooting方法被调用
public void startShooting(){
prevAnimIndex=currAnimIndex;
if(currAnimIndex%2==0){
currAnimIndex++;
}
currAnimation=group.getAnimationStrip(currAnimIndex);
currAnimation.reset();
setState(SHOOTING);
}
//停止射击并回到射击前的画面
public void stopShooting(){
currAnimIndex=prevAnimIndex;
currAnimation=group.getAnimationStrip(currAnimIndex);
currAnimation.reset();
resetState(SHOOTING);
}
//根据指定的方向,让机器人动起来。
public void move(int dir){
//防止过多的射击
resetState(SHOOTING);
switch(dir){
case DIR_NORTH:
if(currAnimIndex!=RobotGroup.WALKING_NORTH){
prevAnimIndex=currAnimIndex;
currAnimation=group.getAnimationStrip(
RobotGroup.WALKING_NORTH);
currAnimIndex=RobotGroup.WALKING_NORTH;
currAnimation.reset();
}else{
animate();
pos.translate(0,-vel.getY());
}
break;
case DIR_SOUTH:
if(currAnimIndex!=RobotGroup.WALKING_SOUTH){
prevAnimIndex=currAnimIndex;
currAnimation=group.getAnimationStrip(
RobotGroup.WALKING_SOUTH);
currAnimIndex=RobotGroup.WALKING_SOUTH;
currAnimation.reset();
}else{
animate();
pos.translate(0,vel.getY());
}
break;
case DIR_WEST:
if(currAnimIndex!=RobotGroup.WALKING_WEST){
prevAnimIndex=currAnimIndex;
currAnimation=group.getAnimationStrip(
RobotGroup.WALKING_WEST);
currAnimIndex=RobotGroup.WALKING_WEST;
currAnimation.reset();
}else{
animate();
pos.translate(-vel.getX(),0);
}
break;
case DIR_EAST:
if(currAnimIndex!=RobotGroup.WALKING_EAST){
prevAnimIndex=currAnimIndex;
currAnimation=group.getAnimationStrip(
RobotGroup.WALKING_EAST);
currAnimIndex=RobotGroup.WALKING_EAST;
currAnimation.reset();
}else{
animate();
pos.translate(vel.getX(),0);
}
break;
default:
break;
}
}
}//Robot
Robot类里需要注意的是static final SHOOTING属性的使用,这里给它赋值8(2的3次方),这样它不会覆盖或者干扰Actor2D类中的状态变量。如果需要复习位操作是如何进行的话,可以参看第2章
现在可以继续进行第二步:创建RobotGroup类。这个类把8个动画系列加载进来,一个是朝各个方向走,一个是朝各个方向开火。然后Robot类可以根据它所面对的方向选择当前的动画条。如果读者有更大的兴趣,可以看看本章的练习,在练习中有一题要求在一次循环中把所有的4个动画都加载进来,这里只是出于演示的目的,在循环中没有这样做。
import java.applet.*;
public class RobotGroup extends ActorGroup2D{
//预先定义的动画序列
public static final int WALKING_NORTH=0;
public static final int SHOOTING_NORTH=1;
public static final int WALKING_SOUTH=2;
public static final int SHOOTING_SOUTH=3;
public static final int WALKING_EAST=4;
public static final int SHOOTING_EAST=5;
public static final int WALKING_WEST=6;
public static final int SHOOTING_WEST=7;
//创建一个新的RobotGroup对象
public RobotGroup(){
super();
animations=new AnimationStrip[8];
}
//初始化8个动画序列
public void init(Applet a){
ImageLoader loader;
int i;
//北
loader=new ImageLoader(a,"robot_north.gif",true);
animations[WALKING_NORTH]=new AnimationStrip();
for(i=0;i<4;i++){
animations[WALKING_NORTH].addFrame(
loader.extractCell((i*72)+(i+1),1,72,80));
}
animations[WALKING_NORTH].setAnimator(new Animator.Looped());
animations[SHOOTING_NORTH]=new AnimationStrip();
for(i=0;i<2;i++){
animations[SHOOTING_NORTH].addFrame(
loader.extractCell((i*72)+(i+1),82,72,80));
}
animations[SHOOTING_NORTH].setAnimator(new Animator.Looped());
//南
loader=new ImageLoader(a,"robot_south.gif",true);
animations[WALKING_SOUTH]=new AnimationStrip();
for(i=0;i<4;i++){
animations[WALKING_SOUTH].addFrame(
loader.extractCell((i*72)+(i+1),1,72,80));
}
animations[WALKING_SOUTH].setAnimator(new Animator.Looped());
animations[SHOOTING_SOUTH]=new AnimationStrip();
for(i=0;i<2;i++){
animations[SHOOTING_SOUTH].addFrame(
loader.extractCell((i*72)+(i+1),82,72,80));
}
animations[SHOOTING_SOUTH].setAnimator(new Animator.Looped());
//东
loader=new ImageLoader(a,"robot_east.gif",true);
animations[WALKING_EAST]=new AnimationStrip();
for(i=0;i<4;i++){
animations[WALKING_EAST].addFrame(
loader.extractCell((i*72)+(i+1),1,72,80));
}
animations[WALKING_EAST].setAnimator(new Animator.Looped());
animations[SHOOTING_EAST]=new AnimationStrip();
for(i=0;i<2;i++){
animations[SHOOTING_EAST].addFrame(
loader.extractCell((i*72)+(i+1),82,72,80));
}
animations[SHOOTING_EAST].setAnimator(new Animator.Looped());
//西
loader=new ImageLoader(a,"robot_west.gif",true);
animations[WALKING_WEST]=new AnimationStrip();
for(i=0;i<4;i++){
animations[WALKING_WEST].addFrame(
loader.extractCell((i*72)+(i+1),1,72,80));
}
animations[WALKING_WEST].setAnimator(new Animator.Looped());
animations[SHOOTING_WEST]=new AnimationStrip();
for(i=0;i<2;i++){
animations[SHOOTING_WEST].addFrame(
loader.extractCell((i*72)+(i+1),82,72,80));
}
animations[SHOOTING_WEST].setAnimator(new Animator.Looped());
}
}
最后的任务是创建一个Applet来容纳机器人并驱动时间循环。在ActorTest类之前,用RobotAdater类做铺垫,RobotAdapter类会监听键被按下和松开的时间。本书认为提供一个外部的类在一个地方处理机器人的动作是一个比较好的方法,这样applet可注册这个适配器从而可以处理击键事件并移动机器人。这里还加入了一段创建平铺绘制背景的代码。
import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;
//控制Robot对象的适配器类
class RobotAdapter extends KeyAdapter{
private Robot robot;
public RobotAdapter(Robot r){
robot=r;
}
//让机器人开火或者移动
public void keyPressed(KeyEvent e){
robot.resetState(Robot.SHOOTING);
switch(e.getKeyCode()){
case KeyEvent.VK_SPACE:
robot.startShooting();
break;
case KeyEvent.VK_UP:
robot.move(Robot.DIR_NORTH);
break;
case KeyEvent.VK_DOWN:
robot.move(Robot.DIR_SOUTH);
break;
case KeyEvent.VK_LEFT:
robot.move(Robot.DIR_WEST);
break;
case KeyEvent.VK_RIGHT:
robot.move(Robot.DIR_EAST);
break;
default:
break;
}
}
//如果空格键松开,则让机器人停止射击
public void keyReleased(KeyEvent e){
if(e.getKeyCode()==KeyEvent.VK_SPACE){
robot.stopShooting();
}
}
}//RobotAdapter
下面是ActorTest:
import java.applet.*;
import java.awt.event.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.util.*;
public class ActorTest
extends Applet implements Runnable {
//动画线程
private Thread animation;
//屏外绘制缓冲
//默然:这个类是在第9章创建的,你需要把它的class文件复制过来
private BufferedGraphics offscreen;
//绘制平铺背景的Paint
Paint paint;
//填充背景的几何形状
private Rectangle2D floor;
//可以移动的机器人
private Robot robot;
public void init() {
//创建RobotGroup
RobotGroup group = new RobotGroup();
group.init(this);
//设置Robot的边界为窗体边界
group.MIN_X_POS = 0;
group.MIN_Y_POS = 0;
group.MAX_X_POS = getSize().width;
group.MAX_Y_POS = getSize().height;
//在屏幕中间创建机器人
robot = new Robot(group);
robot.setPos( (getSize().width - robot.getWidth()) / 2,
(getSize().height - robot.getHeight()) / 2);
//注册一个新的RobotAdapter来接收Robot移动指令
addKeyListener(new RobotAdapter(robot));
//创建背景Paint
createPaint();
offscreen=new BufferedGraphics(this);
AnimationStrip.observer=this;
animation=new Thread(this);
}//init
//创建一个平铺背景Paint
private void createPaint(){
Image image=getImage(getDocumentBase(),"stile.gif");
while(image.getWidth(this)<=0);
//用image的宽和高创建一个新的BufferedImage
BufferedImage bi=new BufferedImage(
image.getWidth(this),
image.getHeight(this),
BufferedImage.TYPE_INT_RGB);
//得到BufferedImage的Graphics2D容器并绘制原始图像
((Graphics2D)bi.getGraphics()).drawImage(image,new AffineTransform(),this);
//以图像的大小创建参照矩形
floor=new Rectangle2D.Double(0,0,getSize().width,getSize().height);
//设置paint
paint=new TexturPaint(bi,new Rectangle(
0,0,image.getWidth(this),image.getHeight(this)));
}
public void start(){
//启动动画线程
animation.start();
}
public void stop(){
animation=null;
}
public void run(){
Thread t=Thread.currentThread();
while(t==animation){
try{
Thread.sleep(10);
}catch(Exception e){
e.printStackTrace();
break;
}
repaint();
}
}//run
public void update(Graphics g){
robot.update();
print(g);
}
public void paint(Graphics g){
Graphics2D bg=(Graphics2D)offscreen.getValidGraphics();
//设置paint并填充背景
bg.setPaint(paint);
bg.fill(floor);
//绘制机器人
robot.paint(bg);
//在窗体上绘制屏外图像
g.drawImage(offscreen.getBuffer(),0,0,this);
}//paint
}//ActorTest
自己运行一下ActorTest applet,看看Robot类到底可以做些什么。
在结束这一章之前,再将Actor2D类快速扩展为StaticActor类。
2.StaticActor类
读者可能已经想过在游戏中实现背景或者其他不运动的物体,这样可以不需要浪费时间一次次地实现静止的物体。考虑把Actor2D类扩展为StaticActor类。
和StaticActorGroup类相比,StaticActor类将对包含单一动画帧的物体提供一种快速的容器。下面给出代码,读者可以自己编写测试程序。
import java.awt.*;
public class StaticActor extends Actor2D{
public StaticActor(ActorGroup2D grp){
super(grp);
//指向第0个动画条
currAnimation=group.getAnimationStrip(0);
frameWidth=currAnimation.getFrameWidth();
frameHeight=currAnimation.getFrameHeight();
}
}
这两个类相当简单,它们描述具有单帧动画并具有场景中移动和旋转能力的物体。在游戏中添加静态画面时,使用这些类是很方便的。关于这个话题可以在第11章看到更多的内容。
注意,我们无须对StaticActor和StaticActorGroup类派生子类——只需要使用正确的参数提供它们的实例。如果需要进行任何特定变换或者其他更新时,可以派生子类,或者让applet类创建调用它的方法。
10.3 总结
不要把本章看作关于使用Java创建游戏实体的正式论文,这些只是用来快速简易地创建游戏实体的正式论文,这些只是用来快速简易地创建游戏的一种方法。有些代码是没有经过优化的,因为在这种方式下思路比较清晰。读者可又根据需要对代码做任何修改,添加,或者删除。游戏编程中一个巨大的挑战是把现有的代码改进为更快,更清晰,更好的代码。
这里只花了几分钟来获得ActorTest applet一个可运行版本并让它运行起来。由于所有主要的后台工作都由Actor2D类及其支撑类提供,为自定义对象定义特定的行为事实上已经成为一件轻而易举的事情。本章提供了很多构建素材,所又读者可能需要再次通读一遍确保所有的内容在头脑中完全掌握。如果确信掌握了所讲的内容,可以派生子类,或者让applet类创建调用它的方法。
10.4 练习
10.1修改Animator类,使用ListIterator对象而不是get方法。对于所有已知的Animator子类而言,ListIterator对象是否比get方法好?对于包含非常多的动画帧的列表而言,使用ListIterator对象会比get方法节省多少时间?
10.2RobotGroup类的init方法,让它在单次循环中加载所有的4组动画序列。读者可能需要把文件名String对象存储为一个数组,这样一切都可又有规律地执行。
10.3创建一对同步的Robot对象,每一个在开始屏幕上处于不同的位置,每一个Robot应该有它自己的监听按键的RobotAdapter对象,两个机器人都应该根据按键反应并又一种同步的方式移动,只不过处于不同的屏幕位置。
10.4一个更好的练习,创建一个applet,包含两个机器人,一个由人控制的RobotAdapter控制,另一个Robot由一个单独的计算机控制机制控制,添加子弹和一点人工智能,就拥有了一个一对一机器人枪战游戏。这个练习可又自由发挥,看看自己可又做的怎样。