詳解連連看遊戲開發流程
遊戲介紹:
“連連看”是一款來源於我國臺灣的桌面小遊戲,主要考驗的是玩家們的眼力,在有限的時間內,只要能把所有能連線的相同圖案,兩個兩個的找出來,每找到一對,它們就會自動消失,只要能把所有的圖案全部消完即可獲得勝利。所謂能夠連線,是指無論橫向還是縱向,從一個圖案到另一個圖案之間的連線拐角不能超過兩個(中間的直線不超過三根),其中連線不能從尚未消去的圖案上經過。
本次開發的連連看遊戲執行效果如下圖所示,遊戲具有統計消去方塊個數、打亂現有方塊位置、智慧輔助以及重開一局的功能。
使用到的素材資料夾如下:
素材及原始碼連結:
遊戲資料模型:
連連看的遊戲介面是一個N*M的網格地圖,每個網格顯示一張圖片;網格地圖的資訊使用二維陣列來儲存,每個陣列元素儲存對應網格地圖中的每一個格子裡的圖片ID,如果圖片ID非-1(BLANK_STATE)則繪製圖片。
動物方塊佈局:
遊戲地圖資訊初始化時,由於方塊必須成對出現,需要引入一個臨時的動態陣列list,該list用來儲存地圖所有的圖案ID資訊,在這裡我們是製作10*10的網格地圖,一共10種圖案,所以可以先向list裡新增10組完全一樣的圖案ID,每組10個;建立二維陣列map儲存網格地圖資訊,初始化map裡的每個陣列元素為-1(BLANK_STATE),然後遍歷map,按遍歷順序依次隨機從list中取一個圖案ID元素放入map並從list中移出剛才取出來的元素,遍歷完成後返回map;程式碼實現如下:
public int[][] getMap(){ ArrayList<Integer> list = new ArrayList<Integer>();//先將等量圖片ID新增到list中 for(int i=0;i<n*n/10;i++){ for(int j=0;j<count;j++){//每個圖案種類的ID各新增一個,迴圈10次 list.add(j); } } for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ int index = (int) (Math.random()*list.size());//從list中隨機取一個圖片ID,並將其新增到陣列中,再從list中刪除掉它 map[i][j] = list.get(index); list.remove(index); } } return map;//返回一個圖片隨機生成的地圖陣列 }
連通演算法:
①直連方式
在直連方式中,要求兩個選中的方塊在同一行或者同一列(圖1-1,圖1-2),並且之間沒有其他任何圖案的方塊,實現最簡單。
②單拐點連通
相當於通過兩個選中的方塊劃出一個矩形,兩個方塊是一對矩形的對角頂點,另外兩個頂點中的某個頂點如果為BLANK_STATE並且可以同時與這兩個方塊直連,那就說明可以單拐點連通(圖1-3)。
③雙柺點連通
這種方式的兩個拐點z1,z2必定在兩個選中的方塊p1,p2所在的水平方向或者垂直方向的直線上(圖1-4,圖1-5)。
按p1(x1,y1)點向4個方向探測(此處的x1,y1為陣列下標),例如向右探測,每次y1+1,判斷(x1,y1+1)與p2(x2,y2)點可否形成單拐點連通性,如果可以形成連通,則兩個拐點連通;如果超出圖形右邊界區域,則還需要判斷兩個拐點在選中方塊的右側,且兩個拐點在圖案區域之外連通的情況是否存在。這裡可以簡化為判斷p2點(x2,y2)是否可以水平直通到右邊界(圖1-6)。
圖1-1圖1-2
圖1-3
圖1-4
圖1-5
圖1-6
經過上面的分析,對兩個選中的方塊是否可以消去演算法流程圖如下:
該功能屬於滑鼠點選事件的一部分,程式碼如下:
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
Graphics g = this.getGraphics();
int x = e.getX()-leftX;//點選位置x-偏移量x
int y = e.getY()-leftY;//點選位置y-偏移量y
int i = y/50;//對應陣列行數,根據畫素座標轉換成陣列下標座標
int j = x/50;//對應陣列列數
if(x<0||y<0)//超出地圖範圍
return ;
if(isClick){//第二次點選
if(map[i][j]!=BLANK_STATE){
if(map[i][j]==clickId){//點選的是相同圖片Id,但不是重複點選同一圖片
if(i==clickX&&j==clickY)
return ;
if(verticalLink(clickX,clickY,i,j)||horizontalLink(clickX,clickY,i,j)||oneCornerLink(clickX,clickY,i,j)||twoCornerLink(clickX,clickY,i,j)){//如果可以連通,畫線連線,然後消去選中圖片並重置第一次選中標識
drawSelectedBlock(j*50+leftX,i*50+leftY,g);
drawLink(clickX,clickY,i,j);//畫線連線
isClick = false;
}else{
clickId = map[i][j];//重新選中圖片並畫框
clearSelectBlock(clickX,clickY,g);
clickX = i;
clickY = j;
drawSelectedBlock(j*50+leftX,i*50+leftY,g);
}
}else{
clickId = map[i][j];//重新選中圖片並畫框
clearSelectBlock(clickX,clickY,g);
clickX = i;
clickY = j;
drawSelectedBlock(j*50+leftX,i*50+leftY,g);
}
}
}else{//第一次點選
if(map[i][j]!=BLANK_STATE){
//選中圖片並畫框
clickId = map[i][j];
isClick = true;
clickX = i;
clickY = j;
drawSelectedBlock(j*50+leftX,i*50+leftY,g);
}
}
}
其中isClick是用來標識是否第一次選中圖案,clickId表示第一次選擇圖案對應的ID,clickX表示第一次選中圖案的行下標,clickY表示第一次選中圖案的列下標,如果第二次選中的圖案與第一次選中的圖案不同,重新選中;如果兩次選擇的圖案相同,但是不連通,重新選中第二次選中的圖片。
直連方式分為水平連通和垂直連通兩種方式,分別使用horizontalLink()和verticalLink()進行判斷:
//判斷是否可以水平相連
private boolean horizontalLink(int clickX1, int clickY1, int clickX2, int clickY2) {
if(clickY1>clickY2){//保證y1<y2
int temp1 = clickX1;
int temp2 = clickY1;
clickX1 = clickX2;
clickY1 = clickY2;
clickX2 = temp1;
clickY2 = temp2;
}
if(clickX1==clickX2){//如果兩個選中圖片的所在行數相同,說明可能可以水平相聯
for(int i=clickY1+1;i<clickY2;i++){
if(map[clickX1][i]!=BLANK_STATE){//如果兩圖片中間還有其他圖片,說明不能直接水平相連
return false;
}
}
linkMethod = LINKBYHORIZONTAL;
return true;
}
return false;
}
//判斷是否可以垂直連線
private boolean verticalLink(int clickX1, int clickY1, int clickX2, int clickY2) {
if(clickX1>clickX2){//保證x1<x2
int temp1 = clickX1;
int temp2 = clickY1;
clickX1 = clickX2;
clickY1 = clickY2;
clickX2 = temp1;
clickY2 = temp2;
}
if(clickY1==clickY2){//如果兩個選中圖片的所在列數相同,說明可能可以垂直相聯
for(int i=clickX1+1;i<clickX2;i++){
if(map[i][clickY1]!=BLANK_STATE){//如果兩圖片中間還有其他圖片,說明不能直接垂直相連
return false;
}
}
linkMethod = LINKBYVERTICAL;
return true;
}
return false;
}
單拐點連通使用oneCornerLink()方法實現判斷(z1儲存拐點陣列下標資訊):
//判斷是否可以通過一個拐點相連
private boolean oneCornerLink(int clickX1, int clickY1, int clickX2, int clickY2) {
if(clickY1>clickY2){//保證(x1,y1)是矩形的左上角或者左下角
int temp1 = clickX1;
int temp2 = clickY1;
clickX1 = clickX2;
clickY1 = clickY2;
clickX2 = temp1;
clickY2 = temp2;
}
if(clickX1<clickX2){//如果(x1,y1)位於矩形左上角
//判斷右上角是否為空並且可以直接與(x1,y1)和(x2,y2)相連線,(clickX1, clickY2)是右上角拐點下標
if(map[clickX1][clickY2]==BLANK_STATE&&horizontalLink(clickX1, clickY1, clickX1, clickY2)&&verticalLink(clickX2,clickY2,clickX1,clickY2)){
linkMethod = LINKBYONECORNER;
z1 = new Node(clickX1,clickY2);
return true;
}
//判斷左下角是否為空並且可以直接與(x1,y1)和(x2,y2)相連線,(clickX2, clickY1)是左下角拐點下標
if(map[clickX2][clickY1]==BLANK_STATE&&horizontalLink(clickX2, clickY2, clickX2, clickY1)&&verticalLink(clickX1,clickY1,clickX2, clickY1)){
linkMethod = LINKBYONECORNER;
z1 = new Node(clickX2,clickY1);
return true;
}
}else{//如果(x1,y1)位於矩形左下角
//判斷左上角是否為空並且可以直接與(x1,y1)和(x2,y2)相連線,(clickX2, clickY1)是左上角拐點下標
if(map[clickX2][clickY1]==BLANK_STATE&&horizontalLink(clickX2, clickY2, clickX2, clickY1)&&verticalLink(clickX1,clickY1,clickX2, clickY1)){
linkMethod = LINKBYONECORNER;
z1 = new Node(clickX2,clickY1);
return true;
}
//判斷右下角是否為空並且可以直接與(x1,y1)和(x2,y2)相連線,(clickX1, clickY2)是右下角拐點下標
if(map[clickX1][clickY2]==BLANK_STATE&&horizontalLink(clickX1, clickY1, clickX1, clickY2)&&verticalLink(clickX2,clickY2,clickX1, clickY2)){
linkMethod = LINKBYONECORNER;
z1 = new Node(clickX1,clickY2);
return true;
}
}
return false;
}
雙柺點連通使用twoCornerLink()方法實現判斷。按p1(clickX1,clickY1)點向4個方向探測新的z1點與p2(clickX2,clickY2)能否行程單拐點連通性(z1,z2儲存兩個拐點的陣列 下標資訊):
//判斷是否可以通過兩個拐點相連
private boolean twoCornerLink(int clickX1, int clickY1, int clickX2, int clickY2) {
//向上查詢
for(int i=clickX1-1;i>=-1;i--){
//兩個拐點在選中圖案的上側,並且兩個拐點在地圖區域之外
if(i==-1&&throughVerticalLink(clickX2, clickY2, true)){
z1 = new Node(-1,clickY1);
z2 = new Node(-1,clickY2);
linkMethod = LINKBYTWOCORNER;
return true;
}
if(i>=0&&map[i][clickY1]==BLANK_STATE){
if(oneCornerLink(i, clickY1, clickX2, clickY2)){
linkMethod = LINKBYTWOCORNER;
z1 = new Node(i,clickY1);
z2 = new Node(i,clickY2);
return true;
}
}else{
break;
}
}
//向下查詢
for(int i=clickX1+1;i<=n;i++){
//兩個拐點在選中圖案的下側,並且兩個拐點在地圖區域之外
if(i==n&&throughVerticalLink(clickX2, clickY2, false)){
z1 = new Node(n,clickY1);
z2 = new Node(n,clickY2);
linkMethod = LINKBYTWOCORNER;
return true;
}
if(i!=n&&map[i][clickY1]==BLANK_STATE){
if(oneCornerLink(i, clickY1, clickX2, clickY2)){
linkMethod = LINKBYTWOCORNER;
z1 = new Node(i,clickY1);
z2 = new Node(i,clickY2);
return true;
}
}else{
break;
}
}
//向左查詢
for(int i=clickY1-1;i>=-1;i--){
//兩個拐點在選中圖案的左側,並且兩個拐點在地圖區域之外
if(i==-1&&throughHorizontalLink(clickX2, clickY2, true)){
linkMethod = LINKBYTWOCORNER;
z1 = new Node(clickX1,-1);
z2 = new Node(clickX2,-1);
return true;
}
if(i!=-1&&map[clickX1][i]==BLANK_STATE){
if(oneCornerLink(clickX1, i, clickX2, clickY2)){
linkMethod = LINKBYTWOCORNER;
z1 = new Node(clickX1,i);
z2 = new Node(clickX2,i);
return true;
}
}else{
break;
}
}
//向右查詢
for(int i=clickY1+1;i<=n;i++){
//兩個拐點在選中圖案的右側,並且兩個拐點在地圖區域之外
if(i==n&&throughHorizontalLink(clickX2, clickY2, false)){
z1 = new Node(clickX1,n);
z2 = new Node(clickX2,n);
linkMethod = LINKBYTWOCORNER;
return true;
}
if(i!=n&&map[clickX1][i]==BLANK_STATE){
if(oneCornerLink(clickX1, i, clickX2, clickY2)){
linkMethod = LINKBYTWOCORNER;
z1 = new Node(clickX1,i);
z2 = new Node(clickX2,i);
return true;
}
}else{
break;
}
}
return false;
}
throughHorizontalLink()用於水平方向判斷邊界的連通性,如flag為true,則從(x,y)點水平向左直到邊界,判斷是否全部為空塊BLANK_STATE;如果flag為false,則從(x,y)點水平向右直到邊界,判斷是否全部為空塊BLANK_STATE。
//根據flag,判斷(x1,y1)左右兩側中的一側是否還有其他圖片,如果沒有,可以相連
private boolean throughHorizontalLink(int clickX, int clickY,boolean flag){
if(flag){//向左查詢
for(int i=clickY-1;i>=0;i--){
if(map[clickX][i]!=BLANK_STATE){
return false;
}
}
}else{//向右查詢
for(int i=clickY+1;i<n;i++){
if(map[clickX][i]!=BLANK_STATE){
return false;
}
}
}
return true;
}
throughVerticalLink()用於水平方向判斷邊界的連通性,如flag為true,則從(x,y)點水平向上直到邊界,判斷是否全部為空塊BLANK_STATE;如果flag為false,則從(x,y)點水平向下直到邊界,判斷是否全部為空塊BLANK_STATE。
//根據flag,判斷(x1,y1)上下兩側中的一側是否還有其他圖片,如果沒有,可以相連
private boolean throughVerticalLink(int clickX,int clickY,boolean flag){
if(flag){//向上查詢
for(int i=clickX-1;i>=0;i--){
if(map[i][clickY]!=BLANK_STATE){
return false;
}
}
}else{//向下查詢
for(int i=clickX+1;i<n;i++){
if(map[i][clickY]!=BLANK_STATE){
return false;
}
}
}
return true;
}
智慧查詢功能實現(按D鍵觸發):
首先先判斷在此之前玩家有沒有選定圖案,如果有清空選定。
選擇第一個方塊:
①從第i行第j列從左向右、從上到下式查詢,如果map[i][j]不為空,選定第一個圖案並記錄選中ID和陣列下標。
選擇第二個方塊:
②從第p行第q列也是從左向右、從上到下式查詢(初始p=i,q=j),如果map[i][j]==map[p][q]並且兩次選中的圖案對應陣列下標不是完全相等,判斷(p,q)和(i,j)是否可以連通,如果可以連通,對兩個方塊進行畫框並連線。如果兩層迴圈下來沒找到可以連通的方塊,重新選定第一個方塊。
如果四層迴圈均未找到連通的方塊,返回false。
//提示,如果有可以連線的方塊就消去並且返回true
private boolean find2Block() {
if(isClick){//如果之前玩家選中了一個方塊,清空該選中框
clearSelectBlock(clickX, clickY, this.getGraphics());
isClick = false;
}
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(map[i][j]==BLANK_STATE){
continue;
}
for(int p=i;p<n;p++){
for(int q=0;q<n;q++){
if(map[p][q]!=map[i][j]||(p==i&&q==j)){//如果圖案不相等或者重複選擇同一個方塊
continue;
}
if(verticalLink(p,q,i,j)||horizontalLink(p,q,i,j)
||oneCornerLink(p,q,i,j)||twoCornerLink(p,q,i,j)){
drawSelectedBlock(j*50+leftX, i*50+leftY, this.getGraphics());
drawSelectedBlock(q*50+leftX, p*50+leftY, this.getGraphics());
drawLink(p, q, i, j);
repaint();
return true;
}
}
}
}
}
isWin();
return false;
}
動物圖案的顯示:
儲存網格地圖資訊的二維陣列map裡儲存的其實是圖片ID,還需要將其轉換成對應的圖片。在這裡使用Image陣列pic存放圖片,圖片資源的獲取及顯示如下:
//初始化圖片陣列
private void getPics() {
pics = new Image[10];
for(int i=0;i<=9;i++){
pics[i] = Toolkit.getDefaultToolkit().getImage("D:/Game/LinkGame/pic"+(i+1)+".png");
}
}
public void paint(Graphics g){
g.clearRect(0, 0, 800, 30);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(map[i][j]!=BLANK_STATE){
g.drawImage(pics[map[i][j]],leftX+j*50,leftY+i*50,50,50,this);
}else{
g.clearRect(leftX+j*50,leftY+i*50,50,50);
}
}
}
}
在這裡如果圖片不是空塊,則按照圖片ID直接繪製圖案,每個方塊的寬度和高度是50,leftX是左上角網格地圖起始X畫素座標,
leftY是左上角起始Y畫素座標。
給選定圖案畫選中框以及清空選中框:
方塊的大小是50*50,在這裡對轉換後的方塊左上角起點(x,y)畫素座標畫框,為什麼在(x+1,y+1)處畫寬度高度為48畫素的矩形呢?因為這次使用了局部重新整理方法,清除選中框時,clearSelectedBlock()會重畫(x,y)處的圖案,這樣48*48的選中框就可以在重畫過程中順利的被清除了。據說這樣做可以消除閃爍。。。
//畫選中框,此處x,y為轉換後的畫素座標
private void drawSelectedBlock(int x, int y, Graphics g) {
Graphics2D g2 = (Graphics2D) g;//生成Graphics物件
BasicStroke s = new BasicStroke(1);//寬度為1的畫筆
g2.setStroke(s);
g2.setColor(Color.RED);
g.drawRect(x+1, y+1, 48, 48);
}
public void clearSelectBlock(int i,int j,Graphics g){
g.clearRect(j*50+leftX, i*50+leftY, 50, 50);
g.drawImage(pics[map[i][j]],leftX+j*50,leftY+i*50,50,50,this);
// System.out.println("清空選定"+i+","+j);
}
畫線及延時功能:
首先先將傳過來的陣列下標進行中心點的轉換,例如map[3][2]對應的方塊左上角座標應該是(2*50+leftX,3*50+leftY),它的中心點座標應該在此基礎上增加半個方塊長寬,即(2*50+leftX+25,3*50+leftY+25);經過這樣的轉換,就可以得到兩個選中的方塊對應的方塊中心座標p1,p2了。
根據連通方式,進行線條的繪畫。
①水平連通或者垂直連通:直接連線p1,p2;
②單拐點連通:將z1和p1,z1和p2進行連線;
③雙柺點連通:如果p1與拐點z1不在同一行或者同一列,先將z1,z2進行交換。再將z1和p1,z2和p2,z1和z2連線。
延時功能:
使用Thread.currentThread().sleep(500);做到畫線後延時500ms再消去方塊。
//畫線,此處的x1,y1,x2,y2二維陣列下標
@SuppressWarnings("static-access")
private void drawLink(int x1, int y1, int x2, int y2) {
Graphics g = this.getGraphics();
Point p1 = new Point(y1*50+leftX+25,x1*50+leftY+25);
Point p2 = new Point(y2*50+leftX+25,x2*50+leftY+25);
if(linkMethod == LINKBYHORIZONTAL || linkMethod == LINKBYVERTICAL){
g.drawLine(p1.x, p1.y,p2.x, p2.y);
System.out.println("無拐點畫線");
}else if(linkMethod ==LINKBYONECORNER){
Point point_z1 = new Point(z1.y*50+leftX+25,z1.x*50+leftY+25);//將拐點轉換成畫素座標
g.drawLine(p1.x, p1.y,point_z1.x, point_z1.y);
g.drawLine(p2.x, p2.y,point_z1.x, point_z1.y);
System.out.println("單拐點畫線");
}else{
Point point_z1 = new Point(z1.y*50+leftX+25,z1.x*50+leftY+25);
Point point_z2 = new Point(z2.y*50+leftX+25,z2.x*50+leftY+25);
if(p1.x!=point_z1.x&&p1.y!=point_z1.y){//保證(x1,y1)與拐點z1在同一列或者同一行
Point temp;
temp = point_z1;
point_z1 = point_z2;
point_z2 = temp;
}
g.drawLine(p1.x, p1.y, point_z1.x, point_z1.y);
g.drawLine(p2.x, p2.y, point_z2.x, point_z2.y);
g.drawLine(point_z1.x,point_z1.y, point_z2.x, point_z2.y);
System.out.println("雙柺點畫線");
}
count+=2;//消去的方塊數目+2
GameClient.textField.setText(count+"");
try {
Thread.currentThread().sleep(500);//延時500ms
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
repaint();
map[x1][y1] = BLANK_STATE;
map[x2][y2] = BLANK_STATE;
isWin();//判斷遊戲是否結束
}
輸贏判斷:
使用isWin()實現,如果當前消去的方塊是10*10個,則遊戲結束。
private void isWin() {
if(count==n*n){
String msg = "恭喜您通關成功,是否開始新局?";
int type = JOptionPane.YES_NO_OPTION;
String title = "過關";
int choice = 0;
choice = JOptionPane.showConfirmDialog(null, msg,title,type);
if(choice==1){
System.exit(0);
}else if(choice == 0){
startNewGame();
}
}
}
打亂現有方塊順序(按A鍵觸發):
這個功能跟前面的隨機生成網格地圖資訊的實現過程很類似,就不多解釋了。
public int[][] getResetMap(){//獲取再次打亂後的地圖資訊
ArrayList<Integer> list = new ArrayList<Integer>();//list用來儲存原先的地圖資訊
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(map[i][j]!=-1)//如果(x,y)處的圖片ID不為-1,那麼將該圖片id新增到list
list.add(map[i][j]);
map[i][j]=-1;
}
}
//將原先地圖上剩餘的未消去的圖片打亂
while(!list.isEmpty()){
int index = (int) (Math.random()*list.size());//從list中隨機取一個圖片ID,並將其新增到陣列中,再從list中刪除掉它
boolean flag = false;
while(!flag){
int i = (int) (Math.random()*n);//獲取隨機的地圖行列
int j = (int) (Math.random()*n);
if(map[i][j]==-1){//如果該位置無圖片
map[i][j] = list.get(index);
list.remove(index);
flag = true;
}
}
}
return map;
}
重開一局:
初始化遊戲面板資料,new地圖物件,重新繪畫即可:
public void startNewGame() {
// TODO Auto-generated method stub
count = 0;
mapUtil = new Map(10,n);
map = mapUtil.getMap();
isClick = false;
clickId = -1;
clickX = -1;
clickY = -1;
linkMethod = -1;
GameClient.textField.setText(count+"");
repaint();
}
到這裡,連連看遊戲開發的核心功能實現已經全部介紹完畢了。
由於本次開發的連連看遊戲原始碼篇幅過長,所以在這裡我就不再貼完整原始碼了,有需要的可以在素材連結裡下載。
如果大家有什麼建議或者對這篇部落格還有疑問的話可以在評論處一起討論,感謝支援~
素材及原始碼連結:https://pan.baidu.com/s/1GEzRACA2PMjFYJZS7hMvTA 提取碼: yc21