書接前文,事表上回。話說上回書提到“畫面閃爍問題和角色動(dòng)作的變更”是目前我們所面臨的兩大難點(diǎn)之一,本次,將就解決畫面閃爍的前提條件——角色動(dòng)作變更,也即“動(dòng)畫”進(jìn)行較為深入的分析。
大家都很清楚的知道,所謂的動(dòng)畫,并不是一個(gè)“會(huì)動(dòng)的畫”,而是一組“連續(xù)變動(dòng)的畫”,就好比Flash制作時(shí)的需要憑借“楨”調(diào)節(jié)畫面運(yùn)動(dòng),在Java游戲開發(fā)中一樣要通過類似的方式來控制畫面。
要實(shí)現(xiàn)這點(diǎn),首先我們需要一組連續(xù)的圖像。
如下圖:
![]()
日常生活中,我們很少會(huì)不知道自己應(yīng)該邁左腳還是邁右腳,但對計(jì)算機(jī)而言,這是我們必須明確提示給他的條件。所以,我們還需要一個(gè)變量充當(dāng)“計(jì)步器”,以明確下步狀態(tài)。
private int count;
而要想實(shí)現(xiàn)動(dòng)畫,最重要的一點(diǎn),就是畫面的連續(xù),即多步操作的處理,為此我們使用到了Java中的Thread,也即線程。
private Thread threadAnime;
在Java中,目前不支持如C#式的函數(shù)直接被線程調(diào)用方式。Java要實(shí)現(xiàn)線程,必需要通過繼承Thread類或?qū)崿F(xiàn)Runnable接口。
我們以Thread類的繼承為例:
//內(nèi)部類,用于處理計(jì)步動(dòng)作。
private class AnimationThread extends Thread {
public void run() {
while (true) {
// count計(jì)步
if (count == 0) {
count = 1;
} else if (count == 1) {
count = 0;
}
// 重繪畫面。
repaint();
// 每300毫秒改變一次動(dòng)作。
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
所謂的繼承,也可以簡單的理解為COPY下所繼承類的全部方法,而這里我們重寫了run()方法,沒有改變其他。也就是說,我們將以自己的方式運(yùn)行AnimationThread這個(gè)類。
另外,在處理drawRole方法時(shí),我們將其內(nèi)部變更如下:
//以count作為圖像的偏移數(shù)值
g.drawImage(roleImage, x*CS, y*CS, x*CS+CS, y*CS+CS,
count*CS, 0, CS+count*CS, CS, this);
推導(dǎo)公式如下圖:

最后,我們在MyPanel構(gòu)建之初即啟動(dòng)線程,令線程的相關(guān)操作活性化。
//實(shí)例化內(nèi)部線程AnimationThread
threadAnime = new Thread(new AnimationThread());
//啟動(dòng)線程
threadAnime.start();
MyPanel代碼如下:
package org.loon.chair.example3;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
/**
* Example3中自定義面板,用于描繪底層地圖。
*
* @author chenpeng
*
* Loon Framework in Game
*
* PS:請注意,此處與前例不同,新增鍵盤事件監(jiān)聽
*/
public class MyPanel extends JPanel implements KeyListener {
//窗體的寬與高
private static final int WIDTH = 480;
private static final int HEIGHT = 480;
//設(shè)定背景方格默認(rèn)行數(shù)
private static final int ROW = 15;
//設(shè)定背景方格默認(rèn)列數(shù)
private static final int
//單個(gè)圖像大小,我默認(rèn)采用32x32圖形,可根據(jù)需要調(diào)整比例。
//當(dāng)時(shí),始終應(yīng)和窗體大小比例協(xié)調(diào);比如32x32的圖片,如何
//一行設(shè)置15個(gè),那么就是480,也就是本例子默認(rèn)的窗體大小,
//當(dāng)然,我們也可以根據(jù)ROW*CS,COl*CS在初始化時(shí)自動(dòng)調(diào)整
//窗體大小,以后的例子中會(huì)用到類似情況。總之一句話,編程
//是[為目的而存在的],所有的方法,大家都可任意嘗試和使用。
private static final int CS = 32;
//設(shè)定地圖,通常在rpg類型游戲開發(fā)中,以[二維數(shù)組]對象為
//基礎(chǔ)進(jìn)行地圖處理,用以描繪出X坐標(biāo)和Y坐標(biāo)。實(shí)際上,即令
//再華麗的RPG類游戲,都是從這些簡單的X,Y坐標(biāo)開始的。
//PS:所謂[數(shù)組],大家可以簡單的理解為即數(shù)據(jù)的集合,一維數(shù)組
//僅包含X軸,而二維是由X,Y兩個(gè)軸組成的,X與Y的交織點(diǎn),即為
//一條數(shù)據(jù)。
private int[][] map = {
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,1,1,1,1,1,0,0,0,0,1},
{1,0,0,0,0,1,0,0,0,1,0,0,0,0,1},
{1,0,0,0,0,1,0,0,0,1,0,0,0,0,1},
{1,0,0,0,0,1,0,0,0,1,0,0,0,0,1},
{1,0,0,0,0,1,1,0,1,1,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}};
//設(shè)定顯示圖像對象
private Image floorImage;
private Image wallImage;
//角色
private Image roleImage;
//角色坐標(biāo)
private int x, y;
//增加計(jì)步器
private int count;
//此處我們添加一組常數(shù),用以區(qū)別左右上下按鍵的觸發(fā),
//之所以采用數(shù)字進(jìn)行區(qū)別,原因大家都很清楚^^,數(shù)字
//運(yùn)算效率高嘛~
private static final int LEFT = 0;
private static final int RIGHT = 1;
private static final int UP = 2;
private static final int DOWN = 3;
private Thread threadAnime;
public MyPanel() {
//設(shè)定初始構(gòu)造時(shí)面板大小
setPreferredSize(new Dimension(WIDTH, HEIGHT));
//于初始化時(shí)載入圖形
loadImage();
//初始化角色所在位置,由于本例行列皆為15,估x與y的極限數(shù)值也皆為15,
//即由15x15的方格圖像,組成了角色的可見活動(dòng)區(qū)域。
x = 8;
y = 8;
//在面板構(gòu)建時(shí)賦予計(jì)步器初值
count = 0;
//設(shè)定焦點(diǎn)在本窗體并付與監(jiān)聽對象
setFocusable(true);
addKeyListener(this);
//實(shí)例化內(nèi)部線程AnimationThread
threadAnime = new Thread(new AnimationThread());
//啟動(dòng)線程
threadAnime.start();
}
//描繪窗體,此處在默認(rèn)JPanel基礎(chǔ)上構(gòu)建底層地圖.
public void paintComponent(Graphics g) {
super.paintComponent(g);
//畫出地圖
drawMap(g);
//畫出人物
drawRole(g);
}
/**
* 載入圖像
*
*/
private void loadImage() {
//獲得當(dāng)前類對應(yīng)的相對位置image文件夾下的地板圖像
ImageIcon icon = new ImageIcon(getClass().getResource("image/floor.gif"));
//將地板圖像實(shí)例付與floorImage
floorImage = icon.getImage();
//獲得當(dāng)前類對應(yīng)的相對位置image文件夾下的墻體圖像
icon = new ImageIcon(getClass().getResource("image/wall.gif"));
//將墻體圖像實(shí)例付與wallImage
wallImage = icon.getImage();
icon = new ImageIcon(getClass().getResource("image/hero.gif"));
roleImage = icon.getImage();
}
/**
* 繪制角色
*/
private void drawRole(Graphics g) {
//以count作為圖像的偏移數(shù)值
g.drawImage(roleImage, x*CS, y*CS, x*CS+CS, y*CS+CS,
count*CS, 0, CS+count*CS, CS, this);
}
private void drawMap(Graphics g) {
//在Java或任何游戲開發(fā)中,算法都是最重要的一步,本例盡使用
//簡單的雙層for循環(huán)進(jìn)行地圖描繪,
for (int x = 0; x < ROW; x++) {
for (int j = 0; j <
// switch作為java中的轉(zhuǎn)換器,用于執(zhí)行和()中數(shù)值相等
// 的case操作。請注意,在case操作中如果不以break退出
// 執(zhí)行;switch函數(shù)將持續(xù)運(yùn)算到最后一個(gè)case為止。
switch (map[x][j]) {
case 0 : //map的標(biāo)記為0時(shí)畫出地板
//在指定位置[描繪]出我們所加載的圖形,以下同
g.drawImage(floorImage, j * CS, x * CS, this);
break;
case 1 : //map的標(biāo)記為1時(shí)畫出城墻
g.drawImage(wallImage, j * CS, x * CS, this);
break;
//我們可以依次類推出無數(shù)的背景組合,如定義椅子為2、寶座為3等
//很容易即可勾勒出一張背景地圖。
default: //當(dāng)所有case值皆不匹配時(shí),將執(zhí)行此操作。
break;
}
}
}
}
public void keyPressed(KeyEvent e) {
//獲得按鍵編號
int keyCode = e.getKeyCode();
//通過轉(zhuǎn)換器匹配事件
switch (keyCode) {
//當(dāng)觸發(fā)Left時(shí)
case KeyEvent.VK_LEFT :
//進(jìn)行left操作,僅符合move()中[規(guī)范]時(shí)執(zhí)行,以下相同
move(LEFT);
break;
//當(dāng)觸發(fā)Right時(shí)
case KeyEvent.VK_RIGHT :
move(RIGHT);
break;
//當(dāng)觸發(fā)Up時(shí)
case KeyEvent.VK_UP :
move(UP);
break;
//當(dāng)觸發(fā)Down時(shí)
case KeyEvent.VK_DOWN :
move(DOWN);
break;
}
// 重新繪制窗體圖像
// PS:在此例程中,僅進(jìn)行了角色的簡單移動(dòng)處理
// ,關(guān)于避免閃爍及限制活動(dòng)區(qū)域問題,請見后續(xù)
// 案例。
repaint();
}
/**
* 用于判定是否允許移動(dòng)的發(fā)生,被move()函數(shù)調(diào)用
* @param x
* @param y
* @return
*/
private boolean isAllow(int x, int y) {
// 以(x,y)交點(diǎn)進(jìn)行數(shù)據(jù)判定,我們都知道,
// 在本例中我僅以0作為地板的參數(shù),1作為
// 墻的參數(shù),由于我們的主角是[人類],而
// 不是[幽靈],所以當(dāng)他要[撞墻]時(shí),我們
// 當(dāng)然不會(huì)允許,至少,是我講到劇情的觸發(fā)
// 以前……
if (map[y][x] == 1) {
// 不允許移動(dòng)時(shí),返回[假]
return false;
}
// 允許移動(dòng)時(shí)時(shí),返回[真]
return true;
}
/**
* 判斷移動(dòng)事件,關(guān)聯(lián)isAllow()函數(shù)
* @param event
*/
private void move(int event) {
//以轉(zhuǎn)換器判斷相關(guān)事件,僅執(zhí)行符合[規(guī)范]的操作。
switch (event) {
case LEFT:
//依次判定事件
if (isAllow(x-1, y)) x--;
break;
case RIGHT:
if (isAllow(x+1, y)) x++;
break;
case UP:
if (isAllow(x, y-1)) y--;
break;
case DOWN:
if (isAllow(x, y+1)) y++;
break;
default:
break;
}
}
/**
* 暫無釋放鍵盤事件
*/
public void keyReleased(KeyEvent e) {
}
/**
* 暫無字符輸入事件
*/
public void keyTyped(KeyEvent e) {
}
//內(nèi)部類,用于處理計(jì)步動(dòng)作。
private class AnimationThread extends Thread {
public void run() {
while (true) {
// count計(jì)步
if (count == 0) {
count = 1;
} else if (count == 1) {
count = 0;
}
// 重繪畫面。
repaint();
// 每300毫秒改變一次動(dòng)作。
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
如何?只是簡單的基點(diǎn)變更,角色已經(jīng)開始走動(dòng)了。
但是,這種單純的左右走動(dòng)效果實(shí)在不好,并非我們所期望般“華麗”,只能算是邯鄲學(xué)步罷了,下一回,我們將講解較為華麗的走動(dòng)“一步蓮華”。
什么時(shí)候才下班啊……555555555~~~
浙公網(wǎng)安備 33010602011771號