Posted on Leave a comment

[14- Pixi教學] PIXI場景設定



載入所使用的Library

在開發遊戲時我們會使用到PixiJS、SystemJS、和JQuery,因此我們會需要將這幾個library加進html裡。
首先我們新增一個index.html的入口檔案,內容如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
  <link rel="stylesheet" type="text/css" href="assets/style.css">
  <script src="lib/system.js"></script>
  <script src="lib/jquery.min.js"></script>
  <script src="lib/pixi.min.js"></script>
</head>
<body>
</body>
</html>

打開這個html後,會看到找不到檔案的訊息。
這時我們在gulpfile.js裡增加一個task名為libs,將我們所需的檔案copy到目標資料夾
其內容如下:

gulp.task('libs', () => {
    return gulp
        .src([
            'node_modules/jquery/dist/jquery.min.js',
            'node_modules/jquery/dist/jquery.min.map',
            'node_modules/systemjs/dist/system-polyfills.js',
            'node_modules/systemjs/dist/system-polyfills.js.map',
            'node_modules/systemjs/dist/system.js',
            'node_modules/systemjs/dist/system.js.map',
            'node_modules/pixi.js/dist/pixi.min.js',
            'node_modules/pixi.js/dist/pixi.min.js.map'
        ])
        .pipe(gulp.dest("build/lib"));
});

這樣子就會在每次編譯時自動複製專案在使用的library進入build/lib裡。

設定場景

要設定一個PixiJS的遊戲場景,這個部份可以先參考關網的範例:Basics
範例裡創建場景的方式如下:

var app = new PIXI.Application(800, 600, {backgroundColor : 0x1099bb});
document.body.appendChild(app.view);

PIXI.Application可以建立一個PIXI的canvas遊戲畫面,其相關API說明請見:PIXI.Application
這個類別有兩種建構子,可以達到相似的功能:

在PixiJS v4以後,官方將所有相關參數都整入options物件內,這樣使用上可以僅傳入需要自行設定的值就好。而要先傳入寬高的則是為了兼容舊版寫法。

new PIXI.Application(960, 540, {width:100, backgroundColor : 0x000000, view: gameCanvasContext});

若兩邊都傳入相同的屬性,會以optionst傳入的值為主。如上面程式碼產生的場景的width會是100。

PIXI.Application可用參數

在創建這個類別時,可以傳入的options參數如下:

Name Type Default Description
autoStart boolean true 過去
width number 800 renderers view的寬
height number 600 renderers view的高
view HTMLCanvasElement 遊戲場景要放置的canvas目標,如果沒有傳入,則會新建一個canvas做為回傳
transparent boolean false 是否支援透明可看到底下物件
antialias boolean false 防鋸齒
preserveDrawingBuffer boolean false 是否支援toDataUrl
resolution number 1 解析度
forceCanvas boolean false 即使存在webGL引擎也不使用
backgroundColor number 0x000000 背景顏色
clearBeforeRender boolean true 在繪圖前先清除canvas上所有元素
roundPixels boolean false 使用Math.floor()來處理所有圖像
forceFXAA boolean false 強制使用FXAA,速度會較快,但可能效果較WebGL差
legacy boolean false 確保能與舊裝置相容
powerPreference string 設定要傳給webGL的參數,對雙顯卡的設備而言可提高效能。
sharedTicker boolean false 可以有共用的PIXI.ticker.shared物件
sharedLoader boolean false 可以有共用的PIXI.loaders.shared物件

於專案中建立場景

接下來,我們在index.html新增一個canvas物件,自行建立一個canvas物件的好處是我們可以較方便的在html內設定canvas的位置

<body>
<div id="gameContainer">
  <canvas id="gameCanvas"></canvas>
</div>
</body>

接著建立Main.ts,其內容如下:

import Application = PIXI.Application;

//將application設定為export,這樣可以在專案的任何地方取到這個元件
export let application:Application
/**
 * 主要的 client application.
 *
 */
export class Main {

    public initGame() {
       //設定場景
       let gameCanvasContext = (< HTMLCanvasElement >jQuery("#gameCanvas")[0]);
       application = new PIXI.Application(960, 540, {width:100,backgroundColor : 0x000000, view: gameCanvasContext});

       //貼一張圖片
       var bunny = PIXI.Sprite.fromImage('assets/bunny.png');
       bunny.x = application.screen.width / 2;
       bunny.y = application.screen.height / 2;
       application.stage.addChild(bunny);
    }
}

接著因為我們使用SystemJS,所以必需用SystemJS的方式來啟動遊戲,在index.html加上下面的javascript script

    var game;
    $(function () {
        //設定要載入的js的位置與副檔名
        SystemJS.config({
            baseURL: "/",
            packages: {
                "/": { defaultExtension: "js" }
            }
        });
        //載入Main.js,載完後執行裡面的某個func
        SystemJS.import('Main').then(function (m) {
            m.Main.prototype.initGame();
        });
    });

這時後打開瀏覽器就可以看到下面的畫面

隨螢幕大小改變尺寸

我們可以看到上面的成果裡,右下方有許多的空白,如果沒有特別對Canvas做縮放,Canvas會一直是相同尺寸。這樣就沒辦法在各個平台、尺寸很順利的進行遊戲,因此要自動偵測並縮放canvas以及場景尺寸。

Main.ts加上onResize()

public onResize() {
    var w = window.innerWidth;
    var h = window.innerHeight;
    var scale = Math.min(w/860,h/540);
    application.view.style.left = (w-scale*860)/2 + "px";
    application.view.style.top = (h-scale*540)/2 + "px";
    application.view.style.width = scale*860 + "px";
    application.view.style.height = scale*540 + "px";
}

並在initGame()新增下面兩行

       //設定遊戲大小隨視窗大小改變
       this.onResize();
       window.onresize = this.onResize; 

這樣遊戲畫面就會永遠置中且自動縮放至適合螢幕大小。



今日成果

原始檔請於此下載:ironman20181029
線上展示:http://claire-chang.com/ironman2018/1029/

Posted on Leave a comment

[13- 遊戲製作] 素材處理

PixiJS能使用的素材

承上一篇文章,我們排好的UI介面,其最終的產出是一個fla檔案,但是這樣的檔案pixiJS是無法使用的。
因此我們必需要整理素材並匯出pixiJS可以使用的素材。

那麼,pixiJS可以使用那些素材呢?最基本的是單張的圖檔,但是因為HTTP/1.1在載入檔案是每個檔案都需額外處理request和response header,會讓下載的速度變慢。並且GPU在處理圖檔上,因GPU擅長矩陣處理,而同一張圖去做截取亦為一種矩陣處理,可以使用到GPU加速的優勢。因此在製作遊戲時,我們偏好使用SpriteSheet。SpriteSheet指的是很多張圖檔被整併到一大張圖檔裡。

如下圖這個動畫:

是由10張圖所構成的,那麼SpriteSheet會將這10張圖打包成一個png檔案。

並會產生一個json檔案,來設定每一個畫面要截取的範圍,其檔案內容如下:
{“frames”: {
“0”:
{
“frame”: {“x”:0,”y”:0,”w”:109,”h”:74},
“rotated”: false,
“trimmed”: true,
“spriteSourceSize”: {“x”:1,”y”:0,”w”:110,”h”:74},
“sourceSize”: {“w”:110,”h”:74}
}
//…
“meta”: {
“app”: “Adobe Animate”,
“version”: “15.1.1.13”,
“image”: “Character_Laugh.png”,
“format”: “RGBA8888”,
“size”: {“w”:256,”h”:512},
“scale”: “1”
}
}
在json裡的”0″,是代表這張圖片的id,之後我們要在pixi去取某個spritesheet的某一張圖,會需要使用到這個id來辨視我們要取的圖。在adobe animate裡,動畫的id他會自動幫我們從0~N這樣以流水號編下去。

另外,在pixiJS裡,某些設備沒辦法處理比4096更大的圖片。
有關於這部份的瀏覽器支援,請見:WebGL Stats

由上圖可看出現在尚有許多設備無法支援4096以上的尺寸的圖檔,這點在包圖時要注意一下

除了以上單張圖檔和SpriteSheet之外,PixiJS還可以使用spine的圖檔,不過由於spine的圖檔編輯軟體要價不斐,因此這次的小作品就先跳過了!

從Adobe Animate匯出動畫連續圖

要從Adobe Animate匯出圖檔,首先要先打開元件庫,然後點擊該動畫元件,按下右鍵,最下方會有一個Generate Sprite Sheet。

接著會跳出一個設定頁面,我們選擇Algorithm為Basic,Data format為JSON,然後按下Export就可以了。

從Adobe Animate匯出圖片集

如果要匯出多個圖檔至一個圖片集,則在元件庫先整理過元件的名稱,預設adobe animate會以元件庫的名稱做為在json內的識別id。
接著一次選擇多個要匯出的圖檔,點右鍵,選擇Generate Sprite Sheet

左邊可以看到將要輸出的sprite sheet裡會有那些圖檔,圖檔的id為何,選項的部份與動畫匯出相同。

音效下載

我是在這邊尋找適合的音效:小森平的免費下載音效

最後打包好的素材集

請按此下載:assets

Posted on Leave a comment

[12- 遊戲製作] 介面設計

連連看遊戲需求

我所規劃的連連看應要有下面的功能:

  • 時間限制:需要在8分鐘內完成遊戲,否則就算是沒有過關。
  • 重整牌面的限制:最多只能重整牌面3次,每次耗1點生命,生命值耗光遊戲就結束了。
  • 音樂、音效需能開或關
  • 重新整理牌面:可以手動洗牌
  • 提示:提示可連線的路徑
  • 復原:還原已消除的圖案
  • 可連到我的FB

其版面配置如下:

尋找免費素材

有了mockup之後,身為一個不會美術的程式設計師,鐵人賽又沒有美術可以幫忙,最重要的就是去尋找免費素材。
這時候就要大推UNITY的Unity Asset Store

很快的我選擇了Free Platform Game Assets這款免費素材來做我連連看的基本素材,首先先登入網站,然後選擇

接著要在電腦裡安裝unity,安裝好之後,再按

接著就會在unity內開啟這個網頁畫面

按下import後,這包素材就會被下載至你剛剛所創的unity專案下的資料夾裡,按右鍵就可以在一般的資料夾找到所下載的素材。

製作遊戲介面

我是使用Adobe Animate來做畫面的繪製。Adobe Animate(前稱Adobe Flash Professional,Macromedia Flash,FutureSplash Animator)是由Adobe Systems開發的多媒體創作和電腦動畫程式。可用於設計向量圖形和動畫,並發布到電視節目,網路影片,網站,網路應用程式,大型網際網路應用程式和電子遊戲中。

這是我所製作完成的遊戲畫面:

原始檔請在此下載:ui.fla

Posted on Leave a comment

[11- 遊戲製作] 使用模組介紹

什麼是NPM

下面是官網上對於NPM的說明

Use npm to install, share, and distribute code; manage dependencies in your projects; and share & receive feedback with others.

簡而言之,NPM是一個套件管理的工具,我們可以使用npm來建立、分享、發佈模組,並且於其平台上分享、接收其他人對模組的反饋。

NPM創造了node的生態圈,我們可以在其官網上搜尋、下載、安裝、使用、並管理我們所需要的模組。

模組的選擇

一般而言,在選擇npm的模組時,要去注意一下這個模組的開發者是否有持續在更新、維護模組內容。並從單周下載量,來評估這個套件的普及度,一般很多人在用的套件會較可信賴。
由於node的生態圈現在還算是個戰國時代,有許多在上面分享的模組很有可能過了一陣子發現了bug,套件的開發者卻已經沒在維護了,我們還得要去研究套件本身是否有某些程式碼會造成這bug,會讓我們在開發時的成本大大增加。

上圖中的weekly download、last publish以及下圖中的Dependents(有多少專案使用這個模組)都是我們要去觀察一個套件的指標。

另外網路上相關教學、使用手冊完整度、網路論壇討論度、是否適合我們的專案,也是我們可拿來做選擇模組的考量點。

我所選擇的套件 – 環境與部署相關

  • run-sequence:可讓gulp的task循序被執行。
        var gulp = require('gulp');
        var runSequence = require("run-sequence");
        gulp.task("build-web", function () {
        runSequence(
            'firstJob',
            'secondJob',
            ['otherJob1', 'otherJob2'],
            function (error) {
                if (error) {
                    console.log(error.message);
                } else {
                    console.log('success');
                }
            }
        );
    });
  • del:可以去刪除本機的檔案。
    var gulp = require('gulp');
    const del = require("del");
    gulp.task('clean', (cb) => {
        return del(["build"], cb);
    });
  • gulp:可以編寫complier時要做的動作的腳本
    var gulp = require('gulp');
    const del = require("del");
    gulp.task('job1', () => {
        //some task write here
    });
  • gulp-typescript:可以使用gulp來呼叫TypeScript的API去將typescript轉為js
    	var ts = require('gulp-typescript');
            var gulp = require('gulp');
            var tsProject = ts.createProject('tsconfig.json');
    	gulp.task('build',  function() {
    	    return tsResult.js.pipe(gulp.dest('build'));
    	});
  • gulp-sourcemaps:可以產生一份xxx.ts.map的檔案,我們可以在debug時連到原始的ts檔案去做偵錯。
  • browser-sync:可以同步瀏覽器,當有檔案被更新時瀏覽器會自動重新整理。
    var gulp = require('gulp');
    const browserSync = require('browser-sync');
    gulp.task('launch-web', ['build-web'], function () {
        browserSync({
            open: true,
            port: 8001,
            files: ["./build/**/*.{html,htm,css,js,json}"],
            server: {
                "baseDir": "./build"
            }
        });
    });

我所選擇的套件 – 開發用Library

  • systemjs:這個套件能夠用模組化的方式來加載檔案,我們只需要在開頭打上下面的程式碼,systemJS就會自動去下載相對路徑的js檔案。
    import {Loader} from "../core/Loader";

    但是若要使用systemJS,則必需在html裡面輸入下面的程式碼,去設定要載入的路徑以及載入檔案的附檔名。

    var game;
    $(function () {
        SystemJS.config({
            baseURL: "",
            packages: {
                "/": { defaultExtension: "js" }
            }
        });
        //載入第一個要執行的檔案
        SystemJS.import('Main').then(function (m) {
            m.Main.prototype.initGame();
        });
    });
  • pixi.js:本系列文章最主要要使用的2D遊戲引擎
  • jquery:大家應該都很熟悉,可以更方便的與html的DOM元件做互動。在開發網頁遊戲時,常常會有某些GUI元件,若使用html來做會更加容易,如線上使用者列表、聊天視窗等…。這時候我們通常會在網頁上重疊兩個div元件,下面那一層放canvas,上面則疊上一層html div元件。如loading page時常是用這樣的方式來製作。妥善的利用canvas與html間不同的優勢,會讓遊戲開發上更加的容易。
  • typescript:TypeScript程式物件導向的特性能讓我們在寫TypeScript的時候,有像是寫強型別的語言一樣輕鬆自在,IDE也可以幫忙檢查基本的錯誤。
  • gsap:一個很好用的tween工具,官網GreenSock裡有非常詳細的使用說明,手冊的範例非常的清楚,在這邊也能夠直接預覽特效的樣子。
Posted on Leave a comment

[10- 遊戲製作] PixiJS介紹

關於PixiJS


下面文字是官網裡對於 Pixi.js 特性的說法:

Pixi.js is a 2D webGL renderer with a seamless canvas fallback that enables it to work across all modern browsers both desktop and mobile. Helping Goodboy and hopefully you achieve the big fat goal of “Build once, play everywhere

他們強調因為Pixi.js支援WebGL的關係,使得Pixi.js的效能表現大大超過過往使用svg做網頁遊戲的效能。詳細比較及實驗數據請參見:突破 D3.js 的速度極限 — 2D WebGL 與 PIXI.js

Pixi.js是一個基於Javascript的2D繪圖引擎,主要是做網頁的2D遊戲,可兼容多種瀏覽器(所支援的瀏覽器版本請參見:What browsers are supported?),但即便是不支援WebGL的瀏覽器,PixiJS也可以正常運行,只是效能會變得差很多,因為沒辦法享受到GPU加速的優勢。

由於Pixi.js的主要開發者過去曾經寫過flash,若是曾經開發過flash的developer,在學習pixi.js時,會感到格外親切。

在Pixi.js裡主要的單張圖片使用的class名稱為Sprite,而動畫則為MovieClip。其他如addChild()removeChildren()play()gotoAndStop()gotoAndPlay()等API的名稱都與flash的語法十分相近。過往的flash developer很容易便能夠透過IDE裡auto complete的功能去找到想要使用的功能。

Pixi.js只有提供關於使用WebGL的圖像處理API以及動畫控制元件,並不一定要我們遵循特定的方式(如:TypeScript、粒子系統)等去做開發。這也是它一個優點,我們可以很自由的去依據自己專案的需求去選擇我們要使用的配套方案。

相關技術 – Phaser


Phaser是一個基於Pixi.js去開發的遊戲框架,雖然Phaser是使用較舊版本的PixiJS為基準,並且我們難以自行將其所使用的PixiJS更新到最新版,而新版的PixiJS提供了更多的功能以及更好的效能。
但是Phaser可以讓我們在開發遊戲時更能夠遵循一致的規範,並且它為我們把許多好用的遊戲開發功能都整合進去,例如粒子系統等。

Phaser額外為Pixi添加了下面的內容:

  • 物理系統的選擇(arcade或full body)
  • 一個遊戲世界和一個可以平移它的鏡頭
  • Tilemap支持
  • 粒子系統
  • 聲音支持
  • 更高級的輸入處理
  • 鍵盤和遊戲手柄輸入
  • Scale Manager可以處理遊戲/場景調整大小+全營幕支持
  • 為Tween物件增加Tween Manager, 使其與核心時間一致
  • 素材載入管理
  • 遊戲狀態管理
  • 遊戲時鐘+自定義計時器+計時器事件

Source:Decide Pixi.js or Phaser

相關技術 – ThreeJS

three.js是另一個可以使用WebGL的3D Javascript遊戲引擎。主要與pixiJS不同的是它是一個3D引擎,而pixiJS主要強項則是產生2D的遊戲。ThreeJS可以讓我們用它創建各種三維場景,包括了攝影機、光影、材質等各種對象。

ThreeJS可以與pixiJS做結合,不過由於3D遊戲是由一個攝影機去做投影,因此投影出來的多個物件會在同一層裡。2D的畫面會永遠在3D物件之上(或之下)而無法交疊出現(以同一個Canvas來說)

這裡是一個將PixiJS結合ThreeJS的範例:https://codepen.io/NEWREBEL/pen/OObLyE

學習PixiJS的好用資源

  • PixiJS API Documentation:裡面有詳細的API手冊,搭配example去學習pixi可以讓上手速度更快速。
  • PixiJS Examples:大部份我們要用PixiJS開發遊戲會需要用到的技術都有很簡單的範例在裡面。
    比較特別的是,Example Code的地方是可以動態修改,然後按上面重整的圖示,就可以即時看到你所修改的東西呈現的樣子,可以讓我們很方便的做測試。
  • Pixi.js官方論壇:我們公司使用PixiJS開發web game已經兩年多了,在這之間遇到難解的怪問題時,這個論壇是一個很好的平台。pixi的主要開發者在論壇裡算是很活躍也很樂於回答我們的問題。他們也會在解惑時提到很多他們建議的開發方式。
  • Pixi教程:一個基於官方英文教學文件的中文翻譯

使用PixiJS開發的作品

這篇介紹的很詳細:[PixiJS – Day-03] 使用 PixiJS 製作的網站,也許並不少

相關資料

Posted on Leave a comment

[9 – 遊戲介紹] 遊戲開發技術介紹

遊戲開發技術介紹

在開發遊戲時,遊戲畫面、特效、音效的呈現是吸引玩家很重要的關鍵,也是讓玩家能夠更加放鬆的享受遊戲內容的必備要素,例如現在強調的虛擬實境等,也都是為了此目的而存在。

因此在開發遊戲程式時,會較一般網頁或應用程式的開發上,花更多的時間與心力在畫面的串接以及動畫邏輯撰寫,讓遊戲畫面能夠與玩家有更多的互動。

如畫面元素的物理性、玩家投擲物體時飛落的路徑、物件間碰撞的偵測與觸發的事件、遊戲角色特定的動作該配合出現的特效、攻擊時的特效粒子系統、特殊效果的畫面濾鏡等等…。

上述的每一個項目都有相關的技術要去學習,可幫助我們更快上手。
如:憤怒鳥所使用的物理引擎為Box2D,物理引擎可以賦與物體物理特性,具有掉落、碰撞等特性;
如:Spine、DragonBones等骨骼動畫套件,壓縮2D遊戲所需要的素材大小。
像這隻龍走動的動畫

可以用這張圖搭配骨骼動畫引擎來組合成上圖的動畫

如:particles.js粒子系統可以讓美術可以使用特效產生器產生特效的設定檔案,並能夠於遊戲中執行。

另外在遊戲開發上,對數學、物理、色彩矩陣等的了解度也是十分重要。

例如Shader是一個能夠透過一連串的數學公式,去針對一個圖片來做出特殊的動畫效果,如水波效果、風吹效果、霧霾、煙霧效果等…。

有了上面這些技術的幫助,我們可以不用每一個特效都要製作連續圖檔,降低美術的負擔,也能讓特效有更多的變化性、更小的遊戲檔案大小。

在學習那些技術之前,我們首先先來認識最基礎的Graphics Library。

Graphics Library

現在的Graphics Library主要的作用,是幫助我們能夠使用GPU來做電腦圖像處理。GPU,圖形顯示器,又稱顯示核心、視覺處理器、顯示晶片或繪圖晶片,是一種專門在個人電腦、工作站、遊戲機和一些行動裝置(如平板電腦、智慧型手機等)上執行繪圖運算工作的微處理器。

在GPU尚未出現前,圖形的繪製是和一般的複雜運算一起使用CPU去做運算,因為影像輸出的處理會需要大量的運算,會讓電腦的運算速度被拖慢。直到1999年8月輝達公司(NVIDIA)在發表GeForce 256繪圖處理晶片時,提出了將影像輸出的顯示晶片,視為獨立的運算單元的概念。
GPU不同於傳統的CPU,傳統CPU內核數量較少專為通用計算而設計。相反,GPU是一種特殊類型的處理器,具有數百或數千個內核,經過最佳化,可並列執行大量計算。雖然GPU在遊戲中以3D彩現而聞名,但它們對執行分析、深度學習和機器學習演算法尤其有用,近年來十分流行的AI人工智慧,由於在大數據處理時需做許多的矩陣計算,也用到GPU其特殊運算的功能。GPU可讓某些計算比傳統CPU上執行相同的計算速度快10倍至100倍。
起初,高效能3D圖像只可經設有3D加速功能(和完全缺乏2D GUI加速功能)的獨立繪圖處理卡上運算,如3dfx的Voodoo。然而,由於製造技術再次進步,影像、2D GUI加速和3D功能都整合到一塊晶片上,現在許多2D的遊戲引擎也都能夠享受GPU所帶來的效益。
有了3D圖形庫,撰寫可以擅用GPU的一個遊戲引擎不再困難重重。以前還沒有統一的API規範時,開發3D遊戲必需要先有配合的GPU硬體廠商,並且針對該硬體去撰寫相關的驅動程式,來使用GPU的運算功能,這會讓開發3D的遊戲困難重重。3D圖形庫最有名的有Direct3D和OpenGL。雖然DirectX在家用市場全面領先,但在專業高端繪圖領域,OpenGL是不能被取代的主角。


OpenGL


OpenGL是個與硬體無關的軟體介面,可以在不同的平台如Windows 95、Windows NT、Unix、Linux、MacOS、OS╱2之間進行移植。這些API規範描述了繪製2D和3D圖形的抽象API。這些API雖然可以完全透過軟體去撰寫,但它是為大部分或者全部使用GPU硬體加速而設計的。OpenGL規範由1992年成立的OpenGL架構評審委員會(ARB)維護。OpenGL不僅語言無關,而且平台無關,在各種平台及設備都有支援OpenGL的介面,移植性較佳。

近年來由於WebGL的推出,讓網頁的3D效果也能夠享受到GPU加速效果,使得3D的遊戲得以在網頁上被實現。Three.js就是一個基於OpenGL使用javascript做開發的3D引擎。

OpenGL是一個不斷進化的API。新版OpenGL規範會定期由Khronos Group發布,通過擴展API來支援各種新功能。

Direct3D


Direct3D是DirectX下的一個子項目。提供設計人員一個共同的硬件驅動標準,讓遊戲開發者不必為每一品牌的硬件來寫不同的驅動程序,也降低用戶安裝及設置硬件的複雜度。

舉個例子,以前在DOS下玩遊戲時,不像現在安裝就可以玩了,過去往往要先設置聲卡的品牌和型號、然後還要設置IRQ(中斷)、I/O(輸入於輸出)、DMA(存取模式),如果哪項設置的不對,那麼遊戲聲音就發不出來。這部分的設置不僅讓玩家傷透腦筋,而且對遊戲開發者來說就更頭痛了。因為為了讓遊戲能夠在眾多電腦中正確運行,開發者必須在開始製作遊戲前,便把市面上所有音效卡的硬體數據都收集過來,然後根據不同的API來寫不同的驅動程式,這對於遊戲製作公司來說,是很難完成的,所以說在當時多媒體遊戲很少。

微軟看到了這個問題,為眾廠家推出了一個共同的應用程序接口——DirectX,只要這個遊戲是依照Directx來開發的,不管你是什麼顯卡、聲卡、統統都能玩,而且還能發揮更佳的效果。當然,前提是你的顯卡、聲卡的驅動程序也必須支持DirectX才行。

以現況來說,OpenGL因為具有OpenGL多平台支援的特色,同一套程式可以直接被套用在各種不同的平台,因此在專業高端繪圖領域,OpenGL是不能被取代的主角,在包含CAD,內容創作,能源,娛樂,遊戲開發,及虛擬實境等行業領域中,OpenGL得到廣泛的應用。DirectX適合於多媒體,娛樂,及時3D動畫等廣泛和實用的3D圖形計算,在基於windows平台上的遊戲開發中,它占領了大部分的市場

遊戲平台介紹

網頁遊戲

網頁遊戲(簡稱頁遊),又稱Web遊戲,是一種基於網頁的電子遊戲,一般不用下載用戶端,任何一台能上網的電腦就可以進行遊戲。與其他大型遊戲比較,具有占用空間小、硬體要求低等特點。
在過去Flash是網頁遊戲最火紅的工具,曾經有一段時間flash遊戲在Facebook上非常受歡迎,如Candy Crush等,都同時有手機遊戲與Facebook版本的遊戲。


智慧型手機遊戲

由於大多數早期手機的效能所限,手機遊戲普遍比較簡單,畫面也比較粗糙,更不要說操作性了(事實上,很少有手機的鍵盤適合玩遊戲的);因此,益智類遊戲(如俄羅斯方塊、貪食蛇、推箱子等)是常見的遊戲類型。目前由於手機以及類手機的PDA裝置發展,手機處理資訊的能力增強,漸漸出現了更大畫面、更加複雜的手機遊戲。現在有更多的公司也推出多人線上手機遊戲。

電腦應用遊戲

大多數的經典遊戲都是電腦應用軟體,如英雄聯盟、魔獸世界、天堂、世紀帝國等等。由於電腦取得容易、又具備語音、易於打字等功能,在線上與網友在互動上更為方便,較大型的多人線上遊戲還是以電腦應用為主。近年來更興起了遊戲直播的風潮,一般實況主常透過直播平台網站轉播遊戲過程、生活瑣事與商業活動,並與觀眾即時性地互動,吸引許多的忠實觀眾。

家用遊戲機

如Wii U、PlayStation 4、Xbox One。電視遊戲,指的是使用電視作為顯示器來遊玩的電子遊戲類型。遊戲由傳輸到「電視」或「類似之音像裝置」的畫面影像(通常包含聲音)構成。遊戲本身通常可以利用連接至遊戲機的掌上型裝置來操控,這種裝置一般被稱作「控制器」或「搖桿」。控制器通常會包含數個「按鈕」和「方向控制裝置」(例如:類比操縱桿),每一個按鈕和操縱桿都會被賦予特定的功能,藉由按下或轉動這些按鈕和操縱桿,操作者可以控制螢幕上的影像。而螢幕、喇叭、和搖桿都可以被整合在一個小型的物件中,被稱作「掌上型電玩」或簡稱「掌機」(Handheld game console)。

遊戲平台

Steam為其中最大的遊戲平台,是美國電子遊戲商維爾福(Valve)於2003年9月12日推出的數位發行平台,提供數位版權管理、多人遊戲、串流媒體和社群網路服務等功能。藉助Steam,用戶能安裝並自動更新遊戲,也可以使用包括好友列表和組在內的社群功能,還能使用雲端儲存、遊戲內語音和聊天功能。Steam軟體免費提供了一個應用程式介面,稱為Steamworks,開發商可以用來整合Steam的功能到自己的產品中,例如網路、線上對戰、成就、微交易,並通過Steam創意工坊分享用戶創作的內容。

遊戲引擎介紹

Unity

Unity是現行最普遍用來開發遊戲的遊戲引擎,可用於開發 Windows、MacOS 及 Linux 平台的單機遊戲,PlayStation、XBox、Wii、3DS 和 任天堂Switch 等遊戲主機平台的電動遊戲,或是 iOS、Android 等行動裝置的遊戲。Unity 所支援的遊戲平台還延伸到了基於 WebGL 技術的 HTML5 網頁。再加上Unity有unity asset store,對於獨立開發者而言,更是一個最好的選擇。Unity遊戲引擎裡本身就含有碰撞偵測、物理引擎、角色模式、玩家視角、光源設定等功能,並且有很完整的IDE介面,並且可支援如Spine等骨骼動畫,對於開發者而言,可以更簡易的製做出一款能吸引人的遊戲。爐石戰記、仙劍6、神魔之塔都是用Unity開發的有名的手機遊戲。

Cocos2d

Cocos2d 是一個開源的2D遊戲框架,在各種不同的程式語言都有不同的實作。一開始在iOS裡支援度最高的遊戲引擎就是Cocos2d,因此有許多iOS的小遊戲都是使用Cocos2D開發的。後來也有了Java版本,可運行於Android平台。Cocos2d-X是基於Cocos2d for iPhone並使用C++語言實現的多平台版本,後來也有了JavaScript版本的Cocos2D。因此現在的cocos2D也可以支援許多不同的平台,但由於其IDE介面不像Unity般強大,所以普及性還是不如Unity。

Flash AIR

雖然現在在網頁應用上,Flash已經幾乎完全被淘汰掉了,但是因為我寫了10幾年的flash一定要介紹一下在手機遊戲上,仍是有許多以Flash AIR所開發的遊戲。相關遊戲作品請見:作品牆

HTML CANVAS

由於WebGL的推出,現在在網頁上開發遊戲,也可以有很不錯的畫面及效能,因此有越來越多的小遊戲都是以H5來製作。網頁遊戲本身具有許多好處,它不需安裝、馬上可以進行遊戲、很容易可以與其他網頁做結合與搭配。對於一些網頁活動行銷專案裡的小遊戲、廣告案、或者去年柯p的政見小遊戲『奔跑吧!台北』,由於其遊戲目標是吸引更多人在遊戲中去了解商品、柯p,更需要能夠完整的與相關網站做連結,這時候H5就是最好的選擇了!
使用H5 Canvas去開發遊戲的缺點是,即使有的webGL的幫助,Javascript效能還是沒辦法與手機原生native code的速度相比,因此較不容易開發全3D又很複雜的遊戲。

連連看技術選擇

由於連連看也是屬於一個很簡單的小遊戲,本身較不會有太大的效能問題,現在網頁遊戲要包成APP也是有許多方法,並且以網頁的特性做開發可以更方便的將遊戲嵌入在不同的平台上。也是我現在最熟悉的技術。所以在這邊我選擇了使用PixiJS+TypeScript來開發連連看遊戲。

參考資料

Posted on Leave a comment

[8 – 遊戲介紹] 遊戲歷史簡介

遊戲的要素

前一篇的成果是連連看的純邏輯程式,沒有具備一般遊戲必備的遊戲畫面、動畫、音效、特效等…。
其成果看起來是這樣的:

只有數字的畫面、沒有連線效果只有文字,一般人很難能夠有耐性的持續玩它,去享受其中遊戲的樂趣。

其實,一款成功的遊戲應有幾個不可或缺的要素:

  • 要素一:畫面及音效
  • 要素二:內容架構
  • 要素三:收費機制
  • 要素四:耐玩度及娛樂性

在前一個單元裡,我們完成了這個遊戲內容架構(遊戲邏輯)的部份,這當然是一款遊戲最核心的部份。但是只有這樣,還沒辦法成為一個真正能夠吸引人的遊戲。一個真正能夠吸引人並要能夠持續經營、生存下去的遊戲,上述的四個要點都是不可或缺的。

以智冠的四川省3000來說,就在相同的連連看機制上加了許多額外的遊戲規則和元素,增加遊戲的遊戲性。

例如在一般的過關模式,增加了角色動畫來加強遊戲視覺的享受,並有時間上的限制來增加遊戲緊張感:

並新增了對戰模式,消除特定的圖示來獲得勝利,並要阻擋對方消除到目標的圖示

增加競賽的元素例如高分榜等,也可以增加玩家競爭的動機,讓黏著度提高。

看到上面的遊戲畫面,是不是覺得比起前一篇的成果更吸引你想要去玩看看呢?

在後面的章節中,我們會慢慢將這些遊戲元素加進我們的連連看遊戲裡面。
現在先讓我們來更深入的從「遊戲的發展歷史」來更深入的了解遊戲產業以及相關成功作品所具備的特色。

喜愛遊戲的我們,就讓我們一起回到年輕時代的我們所熱愛過的遊戲吧!Let’s Go!

遊戲的回憶櫥窗

1970-1983年代

1970年代較流行的是大型的遊戲機台,在那個年代的小朋友,下課時總喜歡跑去遊戲機的店裡投幾個硬幣,來玩幾場遊戲,當作下課最開心的休閒娛樂(然後再被大人抓回家)。

在1980年開始有了掌上型的遊戲機,任天堂的Game & Watch開始生產,這是首次有較小型的遊戲機,如下圖:

Game & Watch遊戲機

1983-1990年代

1983年末至1984年早期,製造北美家庭電腦電子產品的幾間公司紛紛破產,終結了電子遊戲的第二世代。隨著1983年的蕭條,電腦遊戲市場於1984年取代了家用機市場。當時電腦提供同等能支援遊戲的硬體能力,並且因為它們簡單的設計允許遊戲在電腦一開機就完全取得硬體的控制,這種方式相當接近如玩家用機般的簡單直接。

家用遊戲機1983年7月15日在日本以「Family Computer」(FC)為名推出,1985年起在歐美以「Nintendo Entertainment System」(簡稱「NES」)為名發行。紅白機是當時最暢銷的遊戲機,全球累計銷量超過了6100萬台。紅白機出現對電子遊戲產生了十分深遠的影響,讓美國電子遊戲界從1983年的崩潰中恢復過來,1985年發售的超級瑪琍,奠定了FC的初步成功。

後續1986年《勇者鬥惡龍系列》第一部《勇者鬥惡龍》發行,這款遊戲在當時風行了很長的一段時間,相信很多讀者也都對這款遊戲很有印象。這款遊戲在日本文化上造成有史以來的奇蹟,又稱國民RPG。

經典的勇者鬥惡龍

在1987年,最終幻想的第一代出現在市場上,這款遊戲至今仍然是許多人心目中最棒的遊戲,它至今已經出到第15代了,這款遊戲的畫面與音效都獲得很大的好評。它是個效法《勇者鬥惡龍》的角色扮演遊戲 (RPG)。《Final Fantasy系列》的賣座拯救了史克威爾公司免於破產,也是遊戲商心裡最成功的RPG角色扮演遊戲。那時的任天堂在家用遊戲機裡一枝獨秀,奠定了任天堂在當今遊戲界的地位。遊戲機的後續機種為1990年推出的超級任天堂。

最終幻想1

約於同時,《薩爾達傳說系列》首部《薩爾達傳說》在1986年於FC遊戲機上登場。這款遊戲也是很經典且在Wii、DS、3DS等都有製作其續集,近年新出的Switch也有推出『薩爾達傳說 曠野之息』,依然獲得了極高的評價。

1990-2000年代

而在1990年代,隨著個人電腦處理器計算能力的增加以及成本降低,開始出現了使用3D運算的遊戲。在1996年後,隨著個人電腦上使用的平價3D加速卡,3D的遊戲包括RTS遊戲如微軟《世紀帝國》、暴雪娛樂的《魔獸爭霸》與《星海爭霸》系列、以及回合制的遊戲如《魔法門之英雄無敵》,都陸續出現在市場上。

微軟《世紀帝國》遊戲截圖

1990開始也是台灣本土遊戲最興盛的時期,智冠的神州八劍與大宇的軒轅劍都在此時發行。

1995年大宇資訊所開發的經典遊戲《仙劍奇俠傳》,故事裡,李逍遙與林月如、趙靈兒、阿奴的浪漫戀情感動了許多的玩家,堪稱台灣最經典的遊戲。這款遊戲在很長的一段時間都非常的受歡迎,也是當時我非常喜歡的遊戲之一。近年也被製作成3D線上遊戲版本,也有以遊戲故事為主軸所翻拍的電視劇。

1996年的金庸群俠傳也是非常有名的遊戲,玩家不再以旁觀者的身份而進行遊戲,而是真正的融入遊戲之中,在完全自由的金庸武俠世界創造自己的歷史。雖然遊戲的畫面效果在當時看來已經略顯粗糙,但是超強的遊戲性完全彌補了這個缺陷,成為第一款能與《仙劍奇俠傳》相提並論的國產遊戲。

此時期其他台灣遊戲如軒轅劍、神鵰俠侶、天之痕、新絕代雙嬌、大富翁系列等等,也都有了很好的成績。

2000-2010年代

2000年以後的時期,隨著網際網路的普及,BBS開始流行,網路應用也被應用在遊戲上,MMORPG(大型多人在線角色扮演遊戲)開始流行。

早期的線上遊戲有許多是半3D的(俗稱2.5D),因為在此時的機器運算能力下,3D畫面無法製作到很精緻,但是又希望能夠有3D的玩家視角。如【仙境傳說】其背景為3D製作、而遊戲角色的動作等則以2D連續圖為主,我們稱之為2.5D,但其本質仍為3D引擎。

仙境傳說遊戲畫面

其他如石器時代、天堂、魔獸世界說都是這個時期非常流行的線上遊戲。

石器時代遊戲畫面
天堂遊戲畫面

魔獸世界遊戲畫面

在線上遊戲風行的時期裡,台灣的遊戲產業因為未能跟上多人線上遊戲的風潮,單機遊戲的盜版問題日益嚴重,導致台灣本土的遊戲產業漸漸沒落。

2010年至今

在2010年之後智慧型手機、平板電腦、智能電視讓遊戲產業有了許多的挑戰,家用遊戲機也在此時變得更加流行,如任天堂3DS、Wii、任天堂Switch、Xbox One、PlayStation 4都讓遊戲有了更多元的發展,近年來虛擬實境的遊戲也是眾家遊戲廠商注目和投資研發的焦點。。

在手機遊戲上,由於是新的市場,在智能手機剛普及時,有許多的獨立遊戲開發者或是小的遊戲製作團隊,得以加入市場一起競爭,憤怒鳥系列是芬蘭遊戲公司Rovio娛樂出品的電子遊戲系列,便在此時紅了起來。

憤怒鳥遊戲

後來網路遊戲免費模式也開始慢慢佔據一片天,比起計時模式,免費遊戲最大的特點就是不再依據玩家上線時間收費,而是賣出遊戲內特殊道具、SKIN、時裝、遊戲內的特殊幣等來收取費用,而這種商業方式後來在手機遊戲也獲得巨大的成功。

一開始的手機遊戲主要是以下載時付費以下載量來賺取營收,如當時的憤怒鳥、植物大戰僵屍等,以新奇的遊戲方式,且低於5美元甚至1美元的價格,吸引消費者購買,當遊戲下載次數高的時候,營收就非常亮眼,例如憤怒鳥1代於2009年底推出,及至2012年下載次數已突破10億次。後來開始有了遊戲內購買的機制,讓玩家能先免費下載遊戲,增加遊戲的普及度,再藉由遊戲內購機制來獲取營收,後來的遊戲扭蛋抽獎機制更成為手遊市場最大商機。

這個收費模式的興起是因為當時的大部分大陸玩家都無法負擔正版正價遊戲而選擇購買盜版,於是發行商放棄傳統販賣遊戲的模式,改而在免費遊戲中設置商城販賣虛擬商品,其中有部份遊戲提供抽獎箱以此來盈利,結果此抽獎系統大大成功,例如日本的龍族拼圖、怪物彈珠還有這兩年爆紅的FGO,芬蘭的部落衝突-皇室戰爭,香港開發的神魔之塔都屬此一商業模式,他們靠著遊戲黏著度還有不斷更新的新技能或高強度的卡牌(或扭蛋),吸引玩家花更多的錢去抽卡,獲得非常巨大的成功。

但此一商業模式由於機率無法透明,且有賭博的成份,因此歐美中各國紛紛以不同的法律來約束或是禁止,APPLE STORE與GOOGLE STORE也要求軟體供應商必須公布抽獎箱的抽出機率,但抽獎箱無保底機制還是讓許多玩家所詬病,因此有些遊戲有幾連抽(通常需30~50)必有SSR卡(Special Super Rare)的保底機制來吸引玩家抽卡,有些玩家因為無保底機制,花了幾百抽的錢但卻連一張SSR卡都沒有,怒刪遊戲、要求退費或是憤而提告的舉動也屢見不鮮

台灣的手機遊戲公司在這個時期較能成功占領市場的為雷亞遊戲,製作了許多經典的音樂手機遊戲,成為台灣遊戲產業的一顆新星。

在2015後隨著手機越來越普及,線上多人的手機遊戲也更為普及,有更多經典的線上遊戲也都紛紛推出手遊版本,如天堂M、仙境傳說M、石器時代M等。

在下一篇文章裡,我們會介紹現在在開發遊戲時常會用到的幾種技術。

參考資料

Posted on Leave a comment

[7 – 遊戲邏輯] 電腦搜尋路徑



判斷是否存在任一條路徑

在這個連連看遊戲中,是有可能存在死局的,也就是沒有任何兩個圖案可以用兩個轉彎內的線連接起來時。這時我們需要讓電腦能夠自動判斷這種狀況並做出反應,讓玩家可以更明確知道是否有可行的路徑。
那要如何判斷是否存在任一可能路徑呢?有幾個條件:

  • 第一點:電腦能夠判斷連線是否合法
  • 第二點:遍歷所有可能的圖案去確認是否存在可能路徑

第一點在上一篇我們已經做到了,因此在這一篇,我們要找到一個較省時的方式去遍歷所有可能的連線是否存在。

搜尋方法構思

下圖是我在思考電腦自動搜尋時的搜尋邏輯:

因為我希望電腦在搜尋時,能夠避免搜尋已搜尋過的路徑,因此應要紀錄已搜尋過的組合有那些。
為了方便紀錄與判別是否已搜尋過,我決定以圖案來做搜尋依據,由最盤面最左上開始,每遇到一個符號,就判別該符號是否有任何可能可以連線的兩個圖案。
所以我們會需要下面幾件事情:

  • 遍歷盤面,並在找到路徑時停止搜尋
  • 紀錄已搜尋過的符號並避免重複搜尋
  • 列出現有符號可能兩兩連線的所有排列組合
  • 判別兩點間能否連線

程式實作

第四點我們在上一篇文章裡已經完成該功能,因此現在我們應該要做的有1~3項,這部份的程式碼如下:

    //取得第一條搜尋到的已知存在路徑
    public getFirstExistPath():Path{
        var searchedValue = [];//用以紀錄已搜尋過符號
        //由最左上開始做判斷
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                let value = this.board&#91;i&#93;&#91;j&#93;;
                //判斷盤面上現在是有符號的(null代表沒有符號)
                //並且這個符號之前還沒有被搜尋過
                if(value!= null && searchedValue.indexOf(value) == -1){
                    searchedValue.push(value);
                    let positionsArr = this.getPositionByValue(value);//取得盤面上所有這個符號的位置
                    let permutationsArr = this.getPairNumPermutations(positionsArr.length);//取得可能存在的連線的點的排列組合
                    //getPairNumPermutations回傳的格式是&#91;&#91;0,1&#93;,&#91;0,2&#93;,&#91;0,3&#93;,&#91;1,2&#93;,&#91;1,3&#93;,&#91;2,3&#93;&#93;,裡面數字為index
                    //嘗試每一個可能的排列組合
                    for(var k = 0;k<permutationsArr.length;k++){
                        let v = permutationsArr&#91;k&#93;;
                        let path = new Path(positionsArr&#91;v&#91;0&#93;&#93;, positionsArr&#91;v&#91;1&#93;&#93;,this);
                        if(path.canLinkInLine()){
                            return path;
                        }
                    }
                }
            }
        }
        return null;
    }&#91;/code&#93;
<code>getPositionByValue</code>這個函數主要是要取得盤面上所有這個符號的位置,方法內容如下:
[code lang="js"]    public getPositionByValue(value:number):Array<point>{
        let arr = new Array<point>();
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                if (this.board&#91;i&#93;&#91;j&#93; == value){
                    arr.push(new Point(i, j));
                }
            }
        }
        return arr;
    }&#91;/code&#93;
<code>getPairNumPermutations</code>這個函數則是在列出,相同圖案任選2個所有可能的排列組合
如果傳入的是4,也就是C4取2=6,一共會有六種排序的可能性。
查一下維基百科,裡面有相關的解釋:<a href="https://zh.wikipedia.org/wiki/%E7%B5%84%E5%90%88" rel="noopener noreferrer" target="_blank">排列組合</a>
因為兩點間的路徑不會受到先後順序的影響而影響是否能連線,並且相同的點不可連線,這邊我一律讓第一個數字(index)小於第二個數字(index),因此用這個判斷式<code>i != j && i <= j</code>來排除重覆的組合。
用下面的函數,若是同樣的符號有4個,則輸入值為4,輸出值會是[[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]]
[code lang="js"]    private pairNumPermutations = {};
    /**
     * 取得輸入的index中,2個2個一組的所有可能排列組合
     * 回傳的格式是[[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]]
     */
    public getPairNumPermutations(num:number){
        if(this.pairNumPermutations[num] != null){
            return this.pairNumPermutations[num];
        }
        let data = [];
        for(var i = 0; i <num;i++){
            for(var j = 0; j <num;j++){
                if(i != j && i <= j){
                    data.push(&#91;i,j&#93;);
                }
            }
        }
        this.pairNumPermutations&#91;num&#93; = data;
        return data;
    }&#91;/code&#93;

<h3>可行路徑提示</h3>
上面做完了電腦自動搜尋路徑的功能,可以使用在兩個地方,第一個地方是,當盤面沒有任何路徑可以走時,要自動重整盤面。
這部份的程式碼如下:
[code lang="js"]
                            //判斷還有沒有路走
                            if(board.gameRoundEnd()){
                                alert("恭喜完成遊戲!");
                                board = new Board();
                                vm.boardContent = board.board;
                            }else if(board.getFirstExistPath() == null){
                                vm.reloadTimes++;
                                board.rearrangeBoard();//重整盤面
                            }

而重整盤面的程式碼如下:
public rearrangeBoard(){
let values = this.getAllValueInBoard().sort((a, b) => (Math.random() > .5) ? 1 : 0);
for (var i =0;igetAllValueInBoard()所做的事情。
然後再隨機打亂陣列排序,再依序填入盤面上所有有圖案的格子內,就可以達到重整盤面但是不影響到空格的位置。

今日成果

連連看遊戲邏輯至此已大致完成囉!下一篇開始我們會實際開始製作一款實際上具有畫面、音效、特效等真正的網頁連連看遊戲

LIVE DEMO: 今日成果展示
完整程式碼可由此下載:ironman20181022

Posted on Leave a comment

[6 – 遊戲邏輯] 連線消除程式撰寫

主要遊戲流程

連連看點選兩個圖案後,可消除的邏輯是:

  • 兩個所點擊到的圖案相同
  • 連線不超過兩個轉彎

因此我們先來寫遊戲主流程的部份,玩家會先點第一個圖案,代表他想要消除這個圖案,接著再消第二個圖案,這時再來判斷是否符合可消除,這部份的流程圖如下:

撰寫的程式碼如下

var app = angular.module('LianLianKan', []);
app.controller('myCtrl', function ($scope) {
    $scope.select1 = new Point(-1, -1);
    $scope.select2 = new Point(-1, -1);
    $scope.selected = false;
    let msgArra = [];
    $scope.message = msgArra;
    let board = new Board();
    $scope.boardContent = board.board;
    $scope.click = function (x: number, y: number) {
        if ($scope.selected) {
            $scope.select2 = new Point(x, y);
            if (board.hasSameValue($scope.select1, $scope.select2)) {


                if (! ($scope.select1.x == x && $scope.select1.y == y) ) {//確認所選的兩個點不一樣

                    let path = new Path($scope.select1, $scope.select2, board);
                    if(path.canLinkInLine()){
                        board.clearPoint($scope.select1);
                        board.clearPoint($scope.select2);
                        msgArra.push(path);
                    }
                }
            }
            $scope.selected = false;
        } else {
            $scope.select1 = new Point(x, y);
            $scope.selected = true;
        }
    };
});

判斷所選圖案是否相同

在上面的程式碼中,可以看到我們用board.hasSameValue($scope.select1, $scope.select2)來判斷所選的圖是是否相同。我們可以Board的類別增加public hasSameValue(point1: Point, point2: Point): boolean如下:

    public hasSameValue(point1: Point, point2: Point): boolean {
        return this.board[point1.x][point1.y] == this.board[point2.x][point2.y];
    }

連線邏輯撰寫

新建一個類別Path內容如下
class Path {
public point1: Point;
public point2: Point;
readonly board: Board;
public path_Detail:Array;

constructor(point1: Point, point2: Point, board: Board) {
this.point1 = point1;
this.point2 = point2;
this.board = board;
}

public canLinkInLine(): boolean {

console.log(“board”,this.board);

//從上面消
//兩個點都往上找最遠能到達的距離
let point1UP = this.board.getNearByPointByDirection(this.point1, Direction.UP);
let point2UP = this.board.getNearByPointByDirection(this.point2, Direction.UP);
console.log(“point1UP”,point1UP,”point2UP”,point2UP);
//尋找這之中可能存在的路徑
{
let min = Math.max(point1UP.x,point2UP.x);
let max = Math.min(this.point1.x, this.point2.x);
for (var i = max;i>=min;i–){
if (!this.board.hasMiddleValue(new Point(i, this.point1.y), new Point(i, this.point2.y))){
this.path_Detail = [this.point1,new Point(i, this.point1.y),new Point(i, this.point2.y),this.point2];
console.log(“same up”);
return true;
}
}
}
//從下面消
let point1DOWN = this.board.getNearByPointByDirection(this.point1, Direction.DOWN);
let point2DOWN = this.board.getNearByPointByDirection(this.point2, Direction.DOWN);
console.log(“point1DOWN”,point1DOWN,”point2DOWN”,point2DOWN);
{
let max = Math.min(point1DOWN.x,point2DOWN.x);
let min = Math.max(this.point1.x, this.point2.x);
for (var i = min;i<=max;i++){ if (!this.board.hasMiddleValue(new Point(i, this.point1.y), new Point(i, this.point2.y))){ this.path_Detail = [this.point1,new Point(i, this.point1.y),new Point(i, this.point2.y),this.point2]; console.log("same down"); return true; } } } //從左邊消 let point1LEFT = this.board.getNearByPointByDirection(this.point1, Direction.LEFT); let point2LEFT = this.board.getNearByPointByDirection(this.point2, Direction.LEFT); console.log("point1LEFT",point1LEFT,"point2LEFT",point2LEFT); { let min = Math.max(point1LEFT.y,point2LEFT.y); let max = Math.min(this.point1.y, this.point2.y); for (var i = max;i>=min;i–) {
if (!this.board.hasMiddleValue(new Point(this.point1.x, i), new Point(this.point2.x, i))) {
this.path_Detail = [this.point1, new Point(this.point1.x, i), new Point(this.point2.x, i), this.point2];
console.log(“same left”);
return true;
}
}
}

//從右邊消
let point1RIGHT = this.board.getNearByPointByDirection(this.point1, Direction.RIGHT);
let point2RIGHT = this.board.getNearByPointByDirection(this.point2, Direction.RIGHT);
console.log(“point1RIGHT”,point1RIGHT,”point2RIGHT”,point2RIGHT);
{
let max = Math.min(point1RIGHT.y,point2RIGHT.y);
let min = Math.max(this.point1.y, this.point2.y);
for (var i = min;i<=max;i++) { if (!this.board.hasMiddleValue(new Point(this.point1.x, i), new Point(this.point2.x, i))) { this.path_Detail = [this.point1, new Point(this.point1.x, i), new Point(this.point2.x, i), this.point2]; console.log("same right"); return true; } } } //左右連消 if (this.point1.y != this.point2.y){ //先判斷那個點在左,那個點在右 let leftPoint = (this.point1.y < this.point2.y) ? this.point1:this.point2; let rightPoint = (this.point1.y >= this.point2.y) ? this.point1:this.point2;
//取得右邊的點,直線往左最左的那個點
let leftPointRIGHT = this.board.getNearByPointByDirection(leftPoint, Direction.RIGHT);
let rightPointLEFT = this.board.getNearByPointByDirection(rightPoint, Direction.LEFT);
//參考前一篇文章的圖,右邊最左的點不可超過左邊的點,否則會造成誤判
leftPointRIGHT.y = (leftPointRIGHT.y < rightPoint.y) ? leftPointRIGHT.y : rightPoint.y; rightPointLEFT.y = (rightPointLEFT.y > leftPoint.y) ? rightPointLEFT.y : leftPoint.y;
//用迴圈判斷在所有有可能的範圍中是否有可能存在的路徑
if (leftPointRIGHT.y != leftPoint.y && rightPointLEFT.y != rightPoint.y){
for (var i = rightPointLEFT.y; i <= leftPointRIGHT.y; i++) { if (!this.board.hasMiddleValue(new Point(leftPoint.x, i), new Point(rightPoint.x, i))) { this.path_Detail = [leftPoint, new Point(leftPoint.x, i), new Point(rightPoint.x, i), rightPoint]; console.log("same left to right"); return true; } } } } //上下連消 if (this.point1.x != this.point2.x){ let upPoint = (this.point1.x < this.point2.x) ? this.point1:this.point2; let downPoint = (this.point1.x >= this.point2.x) ? this.point1:this.point2;
let upPointDOWN = this.board.getNearByPointByDirection(upPoint, Direction.DOWN);
let downPointUP = this.board.getNearByPointByDirection(downPoint, Direction.UP);
upPointDOWN.x = (upPointDOWN.x < downPoint.x) ? upPointDOWN.x : downPoint.x; downPointUP.x = (downPointUP.x > upPoint.x) ? downPointUP.x : upPoint.x;
if (upPointDOWN.x != upPoint.x && downPointUP.x != downPoint.x){
for (var i = downPointUP.x; i <= upPointDOWN.x; i++) { if (!this.board.hasMiddleValue(new Point(i, upPoint.y), new Point(i, downPoint.y))) { this.path_Detail = [upPoint, new Point(i, upPoint.y), new Point(i, downPoint.y), downPoint]; console.log("same top to down"); return true; } } } } return false; } }[/code] 新增類別Board

class Board {
public board: Array>;

constructor() {
let content = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
//產生初始局面
let length = 10;
let data = content.concat(content).concat(content).concat(content).sort((a, b) => (Math.random() > .5) ? 1 : 0);
this.board = []
for (var i = 0;i= 0; i–) {
if (this.board[i][point.y] == null) {
nearByPoint.x = i;
} else {
break;
}
}
if (nearByPoint.x == 0) {
nearByPoint.x = -1;
}
break;
case Direction.DOWN:
//搜尋往下走最遠可到達的點
let maxLengthDOWN = this.board.length;
for (var i = point.x+1; i < maxLengthDOWN; i++) { if (this.board[i][point.y] == null) { nearByPoint.x = i; } else { break; } } if (nearByPoint.x == maxLengthDOWN - 1) { nearByPoint.x = maxLengthDOWN; } break; case Direction.RIGHT: //搜尋往右走最遠可到達的點 let maxLengthRIGHT = this.board[0].length; for (var i = point.y+1; i < maxLengthRIGHT; i++) { if (this.board[point.x][i] == null) { nearByPoint.y = i; } else { break; } } if (nearByPoint.y == maxLengthRIGHT - 1) { nearByPoint.y = maxLengthRIGHT; } break; case Direction.LEFT: //搜尋往左走最遠可到達的點 for (var i = point.y-1; i >= 0; i–) {
if (this.board[point.x][i] == null) {
nearByPoint.y = i;
} else {
break;
}
}
if (nearByPoint.y == 0) {
nearByPoint.y = -1;
}
break;
}
return nearByPoint;
}

//搜尋在兩個點之中是否可以找到一直線可連接
public canFindPath(a: Point, b: Point, direction:string): boolean {
return this.hasMiddleValue(a ,b);
}

//偵測在兩個點中是否可用一條直線做連接
public hasMiddleValue(a: Point, b: Point): boolean {
let arr = [];
if (a.x == b.x) {
if (a.x == -1 || a.x == this.board.length) return false;
let max = Math.max(a.y, b.y);
let min = Math.min(a.y, b.y);
for (var i = min + 1; i < max; i++) { if (this.board[a.x][i] != null) { return true; } } return false; } else if (a.y == b.y) { if (a.y == -1 || a.y == this.board[0].length) return false; let max = Math.max(a.x, b.x); let min = Math.min(a.x, b.x); for (var i = min + 1; i < max; i++) { if (this.board[i][a.y] != null) { return true; } } return false; } else { return true; } } //判斷某兩個點的值是否相同 public hasSameValue(point1: Point, point2: Point): boolean { return this.board[point1.x][point1.y] == this.board[point2.x][point2.y]; } //將盤面上的圖消掉 public clearPoint(point: Point) { this.board[point.x][point.y] = null; point = null; } } [/code] 今日成果:
Live Demo:今日成果展示
今天的專案可至此下載:ironman20181021

Posted on Leave a comment

[5 – 遊戲邏輯] 圖形連線消除邏輯發想

搜尋邏輯思考

在連連看裡面,連線的線條不可超過兩個轉彎處,兩個轉彎處的意思,代表連接的線最多只能由三條直線來組成。

這時候我們來思考該如何找出這兩點間所存在的那條線。先觀察一下棋盤,最多三條直線,代表有可能是一條直線、兩條直線或三條直線來做連接的。
不論如何,兩個點之間的那條線,一定一邊是從第一個點(A)開始,到另一個點(B)結束,因此,可以視為這兩個點之中,有可能存在A點連出的的(C)點B點連出的(D)點,來形成連線。

由上圖我們可以觀察出,A點連出的的(C)點絕對是和開始的(A)點在同一行或同一列,B點連出的(D)點絕對是和結束的點(B)點在同一行或同一列

尋找可能的線的模型

首先,我們來畫出所有有可能連出來的線圖形狀,再來思考該如何去撰寫消除邏輯。

判斷是否存在此圖形的連線

從上面的圖型我們先來分析向左、上、下、右的邊消的情況,要如何去搜尋可能存在的路徑。
讓我們先看這張圖

圖中的(A)為起始點,(B)為終點,(C)為(A)點最左能走到的點,(D)為(B)點最左能走到的點。則淡紅色漸層的部份,就是存在著可能的路徑。
這個可能連線的區塊的座標應為: 左上(A.x,D.y)、右上(A.x, B.y)、右下(D.x,D.y)、左下(B.x,B.y)。
我們應該要取A_C與B_D的橫向座標(y)中,有交集的部份。

因此這部份的邏輯程式應為

        let potinC = getPathLeftPoint(pointA);
        let pointD = getPathLeftPoint(pointB);
        let min = Math.max(pointC.y,pointD.y);
        let max = Math.min(pointA.y, pointB.y);
        for (var i = max;i>=min;i--) {
            if (!hasMiddleValue(new Point(pointA.x, i), new Point(pointB.x, i))) {
                path = [pointA, new Point(pointA.x, i), new Point(pointB.x, i), pointB];
                return "可消除";
            }
        }

同樣的模型可以套用在向左、向上、向右、向下。
我們可以發現,左右直連可視為A點與C點相疊、B點與D點相疊的向上/向下消除,上下直連亦同。轉折連接可視為A與C或者B與D其中有兩個相疊,另兩個不相疊。都可以用相同的演算法來找出路徑

無法用相同的方式找出來的圖形有這兩個

先來繪製出有可能可以連線的區域

由上圖可知,我們需要找A、B點之間在左邊的(A)點往右可走最多的那個(C)點,然後找A、B點之間在右邊的(B)點往左走最多的(D)點。
然後取出A_C與B_D中y有交集的地方,為有可能可以連線的區域

這部份的邏輯程式碼為
if (pointA.y != pointB.y){
let leftPoint = (pointA.y < pointB.y) ? pointA:pointB; let rightPoint = (pointA.y >= pointB.y) ? pointA:pointB;
let leftPointRIGHT = getPathRightPoint(leftPoint);
let rightPointLEFT = getPathLeftPoint(rightPoint);
leftPointRIGHT.y = (leftPointRIGHT.y < rightPoint.y) ? leftPointRIGHT.y : rightPoint.y; rightPointLEFT.y = (rightPointLEFT.y > leftPoint.y) ? rightPointLEFT.y : leftPoint.y;
if (leftPointRIGHT.y != leftPoint.y && rightPointLEFT.y != rightPoint.y){
for (var i = rightPointLEFT.y; i <= leftPointRIGHT.y; i++) { if (!this.board.hasMiddleValue(new Point(leftPoint.x, i), new Point(rightPoint.x, i))) { this.path_Detail = [leftPoint, new Point(leftPoint.x, i), new Point(rightPoint.x, i), rightPoint]; console.log("same left to right"); return true; } } } } [/code]