用滑鼠拖曳圖案的功能,在許多遊戲裏都能看得到,比如說拼圖遊戲、解謎遊戲、方塊遊戲,或是像暗黑破壞神(Diablo)裏的角色裝備介面,也需要用滑鼠拖曳鎧甲來進行裝備或鑲嵌寶石在盔甲上。
我們今天就是要來寫一個功能完整的物件拖曳系統。
我們今天要寫一個類似Diablo裏,把圖案在格子之間拖來拉去的介面。
專案會畫出物件要對齊的格子。接著會創造一些物件,並讓這些物件去訂閱滑鼠的事件,然後以滑鼠拖曳的設計邏輯去控制物件的移動。最後在放開滑鼠左鍵後,把物件放在離它最近的格子位置上。
雖然專案裏會用到少許CG繪圖的功能,不過我會儘量在每一行都加上說明,希望大家在看程式時不會有閱讀障礙。
在滑鼠拖曳的邏輯中,最重要的就是要讓可拖曳的物件去監聽滑鼠左鍵按下去的事件。
監聽滑鼠,這是承續昨天事件驅動程式的概念。如果不太清楚這個概念的同學,可以先補個課再繼續。
《Trick 19: 事件驅動的程式設計》
當在物件收到滑鼠左鍵按下去的事件時,要先儲存這個時候物件和滑鼠的相對位置,接著要監聽滑鼠移動的事件,以及滑鼠左鍵放開的事件,然後進入拖曳狀態。
在收到滑鼠移動的事件時,利用目前滑鼠的位置以及先前儲存的物件相對位置進行計算,讓物件跟著滑鼠的位置移動。
在收到滑鼠左鍵放開的事件後結束拖曳狀態,將物件對齊格線放好,並取消訂閱滑鼠移動及放開左鍵的事件監聽。
// 定義格子大小
let gridSize = new Size(80, 80);
// 可拖曳物件的類別
class DraggableObj {
// 用CG的功能畫出物件的圖案,詳情請看示範專案
sprite: PIXI.Sprite = ...;
// 用來存物件相對於滑鼠的位置
relPos = new Point();
// 建構子,要給初始位置
constructor(position: Point) {
// 將位置放到初始位置
this.sprite.x = position.x;
this.sprite.y = position.y;
// 訂閱滑鼠左鍵點在this.sprite上的事件(支援觸控螢幕)
this.sprite.on('pointerdown', this.onPointerDown, this);
}
// 收到滑鼠左鍵點在sprite的事件的接收器
private onPointerDown(event: PIXI.InteractionEvent) {
// 如果正在拖曳狀態就不用處理,直接離開
if (this.dragging) {
return;
}
// 進入拖曳狀態
this.dragging = true;
// 從事件中取得滑鼠的位置
let mousePos = event.data.getLocalPosition(this.sprite.parent);
// 稍稍平移物件的位置,才比較容易發現物件被滑鼠抓住了
this.sprite.x += 5;
this.sprite.y += 5;
// 計算目前物件和滑鼠的相對位置
this.relPos.x = this.sprite.x - mousePos.x;
this.relPos.y = this.sprite.y - mousePos.y;
/** 訂閱更多滑鼠事件,因為這些事件不管發生在哪都要收到
* 所以我們要把事件的主人訂在繪圖引擎的根節點
*/
// 訂閱滑鼠移動的事件(支援觸控螢幕)
cg.pixi.root.on('pointermove', this.onPointerMove, this);
// 訂閱滑鼠左鍵放開的事件(支援觸控螢幕)
cg.pixi.root.on('pointerup', this.onPointerUp, this);
}
// 收到滑鼠移動事件的接收器
private onPointerMove(event: PIXI.InteractionEvent) {
// 從事件中取得滑鼠的位置
let mousePos = event.data.getLocalPosition(this.sprite.parent);
// 把sprite的位置移動到與滑鼠相對的正確位置
this.sprite.x = mousePos.x + this.relPos.x;
this.sprite.y = mousePos.y + this.relPos.y;
}
// 收到滑鼠左鍵放開事件的接收器
private onPointerUp(event: PIXI.InteractionEvent) {
const sprite = this.sprite;
// 離開拖曳狀態
this.dragging = false;
// 取消訂閱
cg.pixi.root.off('pointermove', this.onPointerMove, this);
cg.pixi.root.off('pointerup', this.onPointerUp, this);
// 將sprite的位置對齊格線
sprite.position.set(
Math.round(sprite.x / gridSize.width) * gridSize.width,
Math.round(sprite.y / gridSize.height) * gridSize.height
);
}
}
// 建立兩個可拖曳的物件
new DraggableObj(new Point(100, 100));
new DraggableObj(new Point(300, 300));
在示範程式中,會有兩個代表武器的圖案可以用滑鼠拖來拉去,並且在放開滑鼠時自動對齊格線放好。同學們可以以這個範例為基礎,加上更多更完整的功能。這裏給同學們一些Idea:
以這個架構為藍本,還可以製作捲動頁面的捲動棒等更進階的介面拖曳功能。不過再繼續講下去,可能就會偏向個人寫作風格的討論了,所以我們在這兒打住,接下去就看各位同學的發揮了。
有興趣的同學可以看看小哈寫的相關的函式庫:
物件拖曳控制器 DragControl.ts
容器捲動控制器 ScrollControl.ts