This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this tutorial in English
Bu oyun, Bejeweled ve Candy Crush tarzındaki klasik eşleştirme oyununun bir çeşididir. Oyuncu aynı renkteki blokları kaldırmak için sürükleyerek birbirine bağlar. Ancak oyunun amacı aynı renkteki uzun blok dizilerini kaldırmak, tahtayı temizlemek veya puan toplamak değil, tahtanın çeşitli yerlerine dağılmış özel “sihirli blokların” birbirine bağlanmasını sağlamaktır.
Bu öğretici, tamamlanmış bir tasarımdan yola çıkarak oyunu oluşturduğumuz, adım adım ilerleyen bir kılavuz olarak yazılmıştır. Gerçekte işe yarayan bir tasarım bulmak çok zaman ve emek ister. Temel bir fikirle başlayıp ardından bu fikrin neler sunabileceğini daha iyi anlamak için bir prototipini oluşturmanın yolunu bulabilirsiniz. “Magic Link” gibi basit bir oyun bile oldukça fazla tasarım çalışması gerektirir. Bu oyun, son biçimine (hâlâ kusursuz olmaktan çok uzak olsa da) ve oyun kurallarına ulaşana kadar birkaç yinelemeden ve bazı denemelerden geçti. Ancak bu öğreticide o süreci atlayıp son tasarım üzerinden oyunu oluşturmaya başlayacağız.
Yeni bir proje oluşturup varlık (asset) paketini içe aktararak başlamanız gerekir:

Tahta her turda rastgele renkli bloklarla ve bir dizi sihirli blokla doldurulur. Renkli bloklar şu kurallara uyar:
Sihirli bloklar ise şu kurallara göre farklı davranır:
Oyuncu oyunla şu kurallar çerçevesinde etkileşime girer:
Zorluk düzeyi, tahtaya yerleştirilen sihirli blokların sayısını belirler.
Her projede olduğu gibi, uygulamaya genel hatlarıyla nasıl yaklaşacağımızı planlamamız gerekir. Oyunu yapılandırmanın ve oluşturmanın birçok yolu vardır. İsteseydik teknik olarak oyunun tamamını grafik kullanıcı arayüzü (GUI) sistemi içinde geliştirebilirdik. Ancak oyunu oyun nesneleri (game object) ve sprite bileşenleriyle (sprite component) oluşturup ekrandaki GUI ve oyun içi bilgi göstergesi (HUD) öğeleri için GUI API’lerini kullanmak çoğu zaman oyun oluşturmanın doğal yoludur; biz de bu yolu izleyeceğiz.
Dosya sayısının oldukça az kalmasını beklediğimiz için proje klasör yapısını çok basit tutacağız:

game.project dosyasında çoğunlukla varsayılan ayarlar korunur, ancak karar vermemiz gereken bazı ayarlar vardır. Öncelikle oyun için bir çözünürlük seçmemiz gerekir. Çözünürlüğü daha sonra değiştirmek oldukça kolaydır. Oyunun son hâlinde ise hedef cihazın çözünürlüğünden veya en boy oranından bağımsız olarak iyi görünmesini sağlamak için biraz çalışma yapmamız gerekecek.
Çözünürlüğü iPhone 4’ün doğal çözünürlüğü olan 640x960 piksel olarak ayarlamayı seçtik. Bu çözünürlük birçok monitöre de sığdığından bilgisayarda oynanış testi yapmak rahat olur. Farklı bir çözünürlükte çalışmak isterseniz yalnızca birkaç değeri farklı şekilde ayarlamanız gerekir.

Ayrıca işlenebilecek en fazla sprite sayısını artırmamız gerekecek. İsterseniz sonraki bölüme geçebilir ve konsolda sprite sınırına ulaştığınız bildirildiğinde buraya dönebilirsiniz.

Gereken en fazla sprite sayısını hesaplayabiliriz:
En fazla 30 sihirli bloğumuz olduğunu varsayalım. Tahta 63 bloktan (sprite bileşeninden) oluşur. Bunların içindeki 30 sihirli bloğun her biri özel efektler için 4 sprite bileşeni ekler. Bu, 120 ek sprite bileşeni demektir. Dolayısıyla bağlantı grafikleriyle birlikte (bu durumda en fazla 33) her karede en az 120 + 33 = 153 sprite çizmemiz gerekecek. İkinin en yakın kuvveti 256’dır.
Ancak üst sınırı 256 olarak ayarlamak yeterli değildir. Tahtayı her temizleyip sıfırladığımızda mevcut tüm oyun nesnelerini silip yenilerini oluşturacağız. Sprite sayısının, kare boyunca yaşamını sürdüren tüm nesnelere yetecek kadar olması gerekir. Silinen nesneler de buna dahildir; çünkü bunlar karenin sonunda kaldırılır. Bu nedenle en fazla sprite sayısını 512 olarak ayarlamak yeterli olacaktır.
![]()
Oyun için gereken tüm varlıklar önceden hazırlandı. Bunları 512x512 piksellik görüntüler olarak ekliyor ve motorun hedef boyuta küçültmesini sağlıyoruz.
Proje ayarlarında hidpi seçeneğini etkinleştirmek arka arabelleğin yüksek çözünürlüklü olmasını sağlar. Büyük görüntüleri küçülterek çizdiğinizde Retina ekranlarda çok net görünürler.

Bloklara ek olarak bir “connector” (bağlayıcı) görüntüsü ve efekt sprite bileşenleri de bulunur. Ayrıca iki arka plan görüntümüz vardır. Biri oyun tahtasının arka planı, diğeri ana menü için kullanılacak. Tüm görüntüleri images klasörüne ekleyin, ardından sprites.atlas adlı bir atlas dosyası oluşturun. Atlas dosyasını açıp tüm görüntüleri ekleyin.

Düğmeler ve açılır pencereler gibi GUI öğelerini oluşturmak için kullanılan bir dizi GUI görüntüsü vardır. Bunlar gui.atlas adlı ayrı bir atlasa eklenir.
İlk adım tahta mantığını oluşturmaktır. Tahta, oynanış sırasında ekranda görünen her şeyi içeren kendi koleksiyonunda yer alacak. Şimdilik yalnızca “blockfactory” fabrika (factory) bileşeni ve betik gerekiyor. Daha sonra bağlantılar için bir fabrika, ana menü GUI bileşenleri ve son olarak ana menüden oyunu başlatmak için yükleme mekanikleri ile menüye dönmenin bir yolunu ekleyeceğiz.
board.collection dosyasını main klasöründe oluşturun. Daha sonra adresleyebilmek için adını “board” olarak ayarladığınızdan emin olun. Arka plan sprite bileşenini eklerseniz Z konumunu -1 olarak ayarladığınızdan emin olun; aksi takdirde daha sonra oluşturacağımız tüm blokların arkasında çizilmez./main/board.collection yapın.

board.script betik dosyası, tahtanın kendisi ve tahtadaki bloklar için tüm mantığı içerecek. Tahtayı oluşturan işlevi yazarak başlayın ve onu init() içinden (geçici olarak) çağırın. Şu an kullanmayacağımız, ancak daha sonra işimize yarayacak iki işlev daha ekliyoruz:
filter()build_blocklist()Tahta oluşturulduktan sonra tüm blokları içeren iki farklı veri kümesi kullanacağız: self.blocks ve self.board:
-- board.script
go.property("timer", 0) -- Use to time events
local blocksize = 80 -- Distance between block centers
local edge = 40 -- Left and right edge.
local bottom_edge = 50 -- Bottom edge.
local boardwidth = 7 -- Number of columns
local boardheight = 9 -- Number of rows
local centeroff = vmath.vector3(8, -8, 0) -- Center offset for connector gfx since there's shadow below in the block img
local dropamount = 3 -- The number of blocks dropped on a "drop"
local colors = { hash("orange"), hash("pink"), hash("blue"), hash("yellow"), hash("green") }
--
-- filter(function, table)
-- e.g: filter(is_even, {1,2,3,4}) -> {2,4}
--
local function filter(func, tbl)
local new = {}
for i, v in pairs(tbl) do
if func(v) then
new[i] = v
end
end
return new
end
--
-- Build a list of blocks in 1 dimension for easy filtering
--
local function build_blocklist(self)
self.blocks = {}
for x, l in pairs(self.board) do
for y, b in pairs(self.board[x]) do
table.insert(self.blocks, { id = b.id, color = b.color, x = b.x, y = b.y })
end
end
end
--
-- INIT
--
function init(self)
self.board = {} -- Contains the board structure
self.blocks = {} -- List of all blocks. Used for easy filtering on selection.
self.chain = {} -- Current selection chain
self.connectors = {} -- Connector elements to mark the selection chain
self.num_magic = 3 -- Number of magic blocks on the board
self.drops = 1 -- Number of drops you have available
self.magic_blocks = {} -- Magic blocks that are lined up
self.dragging = false -- Drag touch input
msg.post(".", "acquire_input_focus")
msg.post("#", "start_level")
end
local function build_board(self)
math.randomseed(os.time())
local pos = vmath.vector3()
local c
local x = 0
local y = 0
for x = 0,boardwidth-1 do
pos.x = edge + blocksize / 2 + blocksize * x
self.board[x] = {}
for y = 0,boardheight-1 do
pos.y = bottom_edge + blocksize / 2 + blocksize * y
-- Calc z
pos.z = x * -0.1 + y * 0.01 -- <1>
c = colors[math.random(#colors)] -- Pick a random color
local id = factory.create("#blockfactory", pos, null, { color = c })
self.board[x][y] = { id = id, color = c, x = x, y = y }
end
end
-- Build 1d list that we can easily filter.
build_blocklist(self)
end
function on_message(self, message_id, message, sender)
if message_id == hash("start_level") then
build_board(self)
end
end
Tahta mantığı, “blockfactory” fabrika bileşeni aracılığıyla “block” oyun nesneleri oluşturur. Bunun çalışması için blok oyun nesnesini oluşturmamız gerekir. Blokta bir betik ve bir sprite bileşeni bulunur. Sprite bileşeninin varsayılan animasyonunu sprites.atlas içindeki renkli bloklardan herhangi birine ayarlıyor, ardından blok oluşturulduğunda doğru rengi alması için block.script dosyasına kod ekliyoruz:

-- block.script
go.property("color", hash("none"))
function init(self)
go.set_scale_xy(0.18) -- render scaled down without changing Z
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
“blockfactory” fabrika bileşeninin Prototype özelliğini yeni block.go oyun nesnesi dosyasına ayarlayın.

Artık oyunu çalıştırıp tahtanın rastgele renkli bloklarla dolduğunu görebilmelisiniz:

Artık bir tahtamız olduğuna göre kullanıcı etkileşimi eklemeliyiz. Önce input klasöründeki game.input_binding dosyasında girdi eşlemelerini tanımlıyoruz. game.project ayarlarının girdi eşlemeleri dosyanızı kullandığından emin olun.

Yalnızca bir eşlemeye ihtiyacımız var ve MOUSE_BUTTON_LEFT girdisini “touch” eylem adına atıyoruz. Bu oyun çoklu dokunma kullanmaz ve kolaylık sağlamak için Defold tek parmakla dokunma girdisini sol fare tıklamalarına dönüştürür.
Girdiyi işleme görevi tahtaya düştüğünden bunun için board.script dosyasına kod eklememiz gerekir:
-- board.script
function on_input(self, action_id, action)
if action_id == hash("touch") and action.value == 1 then
-- What block was touched or dragged over?
local x = math.floor((action.x - edge) / blocksize)
local y = math.floor((action.y - bottom_edge) / blocksize)
if x < 0 or x >= boardwidth or y < 0 or y >= boardheight or self.board[x][y] == nil then
-- outside board.
return
end
if action.pressed then
-- Player started touch
msg.post(self.board[x][y].id, "make_orange")
self.dragging = true
elseif self.dragging then
-- then drag
msg.post(self.board[x][y].id, "make_green")
end
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
end
end
make_orange ve make_green iletileri (message), kodun çalıştığını görsel olarak doğrulamak için yalnızca geçici olarak kullanılır. Bu iletileri işlemek için block.script dosyasına kod eklememiz gerekir:
-- block.script
function on_message(self, message_id, message, sender)
if message_id == hash("make_orange") then
sprite.play_flipbook("#sprite", hash("orange"))
elseif message_id == hash("make_green") then
sprite.play_flipbook("#sprite", hash("green"))
end
end
Artık dokunduğunuz (veya fare düğmesini basılı tuttuğunuz) sürece bloklara önce bir make_orange iletisi, ardından make_green iletileri gönderilecektir. Bu nedenle bloklar yeşile dönmeden önce muhtemelen yalnızca bir anlığına turuncu yanıp söneceklerdir (hatta bu bile olmayabilir). Ancak oyuncunun hangi bloğa dokunduğunu biliyoruz! Girdinin nasıl işlendiğini daha ayrıntılı izlemek isterseniz koda print() veya pprint() çağrıları ekleyin.
Şimdi blokların oyuncu tarafından bağlandığını göstermek için kullanılacak işaretleyicinin varlıklarına ihtiyacımız var. Amaç, her bloğun bağlı olduğunu göstermek için üzerine bir grafik yerleştirmektir.
Bağlayıcının sprite görüntüsünü tutan bir “connector” oyun nesnesi ve “board” oyun nesnesinde bir “connector factory” fabrika bileşeni oluşturmamız gerekir:


Bu oyun nesnesinin betiği çok kısadır; yalnızca grafikleri oyunun geri kalanıyla uyumlu olacak şekilde ölçeklemesi ve Z sırasını doğru ayarlaması gerekir.
-- connector.script
function init(self)
go.set_scale_xy(0.18) -- Scale in 2D without changing Z.
go.set(".", "position.z", 1) -- Put on top.
end
same_color_neighbors() işlevi, belirli bir bloğa (x, y konumundaki) komşu olan ve onunla aynı renkteki blokların listesini döndürür. Bu işlev, self.blocks içindeki blokların tamamını içeren tek düzeyli listeye uygulanan filter() işlevini kullanır.
-- board.script
--
-- Returns a list of neighbor blocks of the same color as the
-- block on x, y
--
local function same_color_neighbors(self, x, y)
local f = function (v)
return (v.id ~= self.board[x][y].id) and
(v.x == x or v.x == x - 1 or v.x == x + 1) and
(v.y == y or v.y == y - 1 or v.y == y + 1) and
(v.color == self.board[x][y].color)
end
return filter(f, self.blocks)
end
in_blocklist() yardımcı işlevi, bir bloğun blok listesinde bulunup bulunmadığını denetler:
-- board.script
--
-- Does the block exist in the list of blocks?
--
local function in_blocklist(blocks, block)
for i, b in pairs(blocks) do
if b.id == block then
return true
end
end
return false
end
Bu işlevleri, dokunarak seçilen blok zincirini oluşturmak için on_input() içindeki dokunma ve sürükleme girdileri sırasında kullanıyoruz. Henüz sihirli bloklar bulunmasa da burada onları denetleyip yok sayacağız:
-- board.script
function on_input(self, action_id, action)
...
-- If trying to manipulate magic blocks, ignore.
if self.board[x][y].color == hash("magic") then
return
end
if action.pressed then
-- List of neighbors of the same color as touched block
self.neighbors = same_color_neighbors(self, x, y)
self.chain = {}
table.insert(self.chain, self.board[x][y])
-- Mark block.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
self.dragging = true
elseif self.dragging then
-- then drag
if in_blocklist(self.neighbors, self.board[x][y].id) and not in_blocklist(self.chain, self.board[x][y].id) then
-- dragging over a same-colored neighbor
table.insert(self.chain, self.board[x][y])
self.neighbors = same_color_neighbors(self, x, y)
-- Mark block.
p = go.get_position(self.board[x][y].id)
local id = factory.create("#connectorfactory", p + centeroff)
table.insert(self.connectors, id)
end
end
Son olarak, dokunma bırakıldığında tüm bağlantı işaretlerini görünümden kaldırın.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
-- Empty chain of connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
end

Artık aynı renkteki blokların bağlanmasını sağlayan mantık hazır ve bağlanan blokları kaldırmak kolay. Tahtadaki konumu doğrudan nil yapmak yerine hash("removing") olarak ayarlamamızın nedeni, daha sonra sihirli blok mantığını yazarken sihirli blokların yalnızca yeni kaldırılmış blokların yerine kaymasını sağlamamızın gerekmesidir. Tahtadaki konumu burada nil olarak ayarlarsak yeni kaldırılmış bloklarla daha önce kaldırılan blokları ayırt edemeyiz.
-- board.script
-- Remove the currently selected block-chain
--
local function remove_chain(self)
-- Delete all chained blocks
for i, c in ipairs(self.chain) do
self.board[c.x][c.y] = hash("removing")
go.delete(c.id)
end
self.chain = {}
end
Tahtada hash("removing") olarak ayarlanmış konumları gerçekten kaldırmak (nil yapmak) için de bir işleve ihtiyacımız olacak:
-- board.script
--
-- Set removed blocks to nil
--
local function nilremoved(self)
for y = 0,boardheight - 1 do
for x = 0,boardwidth - 1 do
if self.board[x][y] == hash("removing") then
self.board[x][y] = nil
end
end
end
end
Ayrıca altlarındaki bloklar kaldırıldığında (nil yapıldığında) kalan blokları aşağı kaydıran bir işlev oluşturuyoruz. Tahtayı sütun sütun soldan sağa dolaşıyor ve her sütunu aşağıdan yukarıya inceliyoruz. Boş (nil) bir konumla karşılaşırsak bu konumun üzerindeki tüm blokları aşağı kaydırıyoruz.
-- board.script
--
-- Apply shift-down logic to all blocks.
--
local function slide_board(self)
-- Slide all remaining blocks down into blank spots.
-- Going column by column makes this easy.
local dy = 0
local pos = vmath.vector3()
for x = 0,boardwidth - 1 do
dy = 0
for y = 0,boardheight - 1 do
if self.board[x][y] ~= nil then
if dy > 0 then
-- Move down dy steps
self.board[x][y - dy] = self.board[x][y]
self.board[x][y] = nil
-- Calc new position
self.board[x][y - dy].y = self.board[x][y - dy].y - dy
go.animate(self.board[x][y-dy].id, "position.y", go.PLAYBACK_ONCE_FORWARD, bottom_edge + blocksize / 2 + blocksize * (y - dy), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x][y-dy].id, "position.z", x * -0.1 + (y-dy) * 0.01)
end
else
dy = dy + 1
end
end
end
-- blocklist needs updating
build_blocklist(self)
end

Artık dokunma bırakıldığında ve self.chain içinde bloklar olduğunda bu işlevleri çağıracak kodu on_input() içine ekleyebiliriz.
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board and slide the remaining blocks down.
remove_chain(self)
nilremoved(self)
slide_board(self)
end
-- Empty chain of connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Şimdi sihirli blokları ekleme zamanı. Öncelikle blokların sihirli bloklara dönüşebilmesini sağlayalım. Böylece doldurulmuş tahtayı ayrı bir geçişte dolaşıp istediğimiz blokları sihirli bloklara dönüştürebiliriz. Sihirli blokları biraz daha ilgi çekici hâle getirmek için önce sihirli bloktan oluşturabileceğimiz magic_fx.go oyun nesnesi biçiminde animasyonlu bir sihir efekti oluşturalım.

Bu oyun nesnesi iki sprite bileşeni içerir. Biri “magic” rengidir (magic-sphere_layer2.png görüntüsünü kullanan bir sprite bileşeni), diğeri ise “light” adlı bir ışık efektidir (magic-sphere_layer3.png görüntüsünü kullanan bir sprite bileşeni). Nesne, oluşturulduğunda direction özelliğinin değerine bağlı olarak dönecek şekilde ayarlanır. Ayrıca nesnenin, ışık efekti sprite bileşenini denetleyen iki iletiyi dinlemesini sağlarız: lights_on ve lights_off.
Yeni bir betik oluşturun ve bunu magic_fx.go nesnesine betik bileşeni olarak ekleyin:
-- magic_fx.script
go.property("direction", hash("left"))
function init(self)
msg.post("#", "lights_off")
if self.direction == hash("left") then
go.set(".", "euler.z", 0)
go.animate(".", "euler.z", go.PLAYBACK_LOOP_FORWARD, 360, go.EASING_LINEAR, 3 + math.random())
else
go.set(".", "euler.z", 0)
go.animate(".", "euler.z", go.PLAYBACK_LOOP_FORWARD, -360, go.EASING_LINEAR, 2 + math.random())
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("lights_on") then
msg.post("#light", "enable")
elseif message_id == hash("lights_off") then
msg.post("#light", "disable")
end
end
Artık sihirli blok, make_magic iletisini aldığında iki magic_fx oyun nesnesi oluşturacak. Bunların her biri zıt yönde dönerek blokların içinde hoş bir renk dansı yaratacak. Ayrıca block.go nesnesine magic-sphere_layer4.png görüntüsünü kullanan ek bir sprite bileşeni ekliyoruz. Bu görüntü, oluşturulan efektinkinden daha yüksek bir Z konumuna yerleştirilir ve sihirli kürenin kabuğunu ya da “cover” adı verilen kapağını çizer.

Blok oyun nesnesine bir Factory bileşeni eklememiz ve Prototype olarak magic_fx.go oyun nesnemizi kullanmasını sağlamamız gerektiğine dikkat edin. Blok betiğinin de lights_on ve lights_off iletilerini dinlemesi ve bunları oluşturulan nesnelere iletmesi gerekir. Blok silindiğinde oluşturulan nesnelerin de silinmesi gerektiğini unutmayın. Bu işlem, bloğun final() işlevinde yapılır. Bunların tümü block.script içinde gerçekleşir.
-- block.script
function init(self)
go.set_scale_xy(0.18) -- render scaled down without changing Z
self.fx1 = nil
self.fx2 = nil
msg.post("#cover", "disable")
if self.color ~= nil then
sprite.play_flipbook("#sprite", self.color)
else
msg.post("#sprite", "disable")
end
end
function final(self)
if self.fx1 ~= nil then
go.delete(self.fx1)
end
if self.fx2 ~= nil then
go.delete(self.fx2)
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("make_magic") then
self.color = hash("magic")
msg.post("#cover", "enable")
msg.post("#sprite", "enable")
sprite.play_flipbook("#sprite", hash("magic-sphere_layer1"))
self.fx1 = factory.create("#fxfactory", p, nil, { direction = hash("left") })
self.fx2 = factory.create("#fxfactory", p, nil, { direction = hash("right") })
go.set_parent(self.fx1, go.get_id())
go.set_parent(self.fx2, go.get_id())
go.set(self.fx1, "position.z", 0.01)
go.set(self.fx1, "scale.xy", 1)
go.set(self.fx2, "position.z", 0.02)
go.set(self.fx2, "scale.xy", 1)
elseif message_id == hash("lights_on") or message_id == hash("lights_off") then
msg.post(self.fx1, message_id)
msg.post(self.fx2, message_id)
end
end
Artık sihirli bloklar oluşturabilir ve ışıklarını yakabiliriz. Bu efekti, bir sihirli bloğun başka bir sihirli bloğun yanında bulunduğunu göstermek için kullanacağız.

Tahtayı bloklarla dolduran kodu, tahtaya bazı sihirli bloklar da ekleyecek şekilde değiştirmemiz gerekiyor:
-- board.script
local function build_board(self)
...
-- Distribute magic blocks.
local rand_x = 0
local rand_y
for y = 0, boardheight - 1, boardheight / self.num_magic do
local set = false
while not set do
rand_y = math.random(math.floor(y), math.min(boardheight - 1, math.floor(y + boardheight / self.num_magic)))
rand_x = math.random(0, boardwidth - 1)
if self.board[rand_x][rand_y].color ~= hash("magic") then
msg.post(self.board[rand_x][rand_y].id, "make_magic")
self.board[rand_x][rand_y].color = hash("magic")
set = true
end
end
end
-- Build 1d list that we can easily filter.
build_blocklist(self)
end
Sihirli blokların temel mekaniği, yanlarındaki başka bir blok kaybolduğunda yana kayabilmeleridir. Bu mekaniğin tüm ayrıntılarını board.script içindeki slide_magic_blocks() işlevine yansıtıyoruz. Algoritma basittir:
M listesi oluşturun.M listesi küçülmeyi bırakana kadar listedeki her sihirli bloğu dolaşın. Her yinelemede:
hash("removing") değerine sahip bir blok konumu varsa bloğu yalnızca M listesinden kaldırın.hash("removing") olarak işaretlenmiş bir boşluk varsa bloğu oraya kaydırın, eski konumunu hash("removing") olarak ayarlayın ve ardından M listesinden kaldırın.-- board.script
-- Apply the shifting logic to magic blocks. Only slide to positions
-- marked for removal with hash("removing")
--
local function slide_magic_blocks(self)
-- Slide all magic blocks to the side that should slide first.
-- This works best going row by row!
local row_m
for y = 0,boardheight - 1 do
row_m = {}
-- Build list of magic blocks on this row.
for x = 0,boardwidth - 1 do
if self.board[x][y] ~= nil and self.board[x][y] ~= hash("removing") and self.board[x][y].color == hash("magic") then
table.insert(row_m, self.board[x][y])
end
end
local mc = #row_m + 1
-- Go through list, slide and remove if possible. Reiterate until the list does not shrink.
while #row_m < mc do
mc = #row_m
for i, m in pairs(row_m) do
local x = m.x
if y > 0 and self.board[x][y-1] == hash("removing") then
-- Hole below, do nothing.
row_m[i] = nil
elseif x > 0 and self.board[x-1][y] == hash("removing") then
-- Hole to the left! Slide magic block there
self.board[x-1][y] = self.board[x][y]
self.board[x-1][y].x = x - 1
go.animate(self.board[x][y].id, "position.x", go.PLAYBACK_ONCE_FORWARD, edge + blocksize / 2 + blocksize * (x - 1), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x][y].id, "position.z", (x - 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Will be nilled later
row_m[i] = nil
elseif x < boardwidth - 1 and self.board[x + 1][y] == hash("removing") then
-- Hole to the right. Slide magic block there
self.board[x+1][y] = self.board[x][y]
self.board[x+1][y].x = x + 1
go.animate(self.board[x+1][y].id, "position.x", go.PLAYBACK_ONCE_FORWARD, edge + blocksize / 2 + blocksize * (x + 1), go.EASING_OUTBOUNCE, 0.3)
-- Calc new z
go.set(self.board[x+1][y].id, "position.z", (x + 1) * -0.1 + y * 0.01)
self.board[x][y] = hash("removing") -- Will be nilled later
row_m[i] = nil
end
end
end
end
end
on_input() içinde bu işlevi çağırarak mekaniği deneyebiliriz:
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Slide remaining blocks down.
slide_board(self)
end
self.chain = {}
-- Empty chain clears connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Artık konumları kaldırırken neden ara bir hash("removing") “etiketi” kullandığımızı açıkça görüyoruz. Bu etiket olmasaydı sihirli bloklar yanlarındaki herhangi bir boş konuma ileri geri kayardı. Belki ilginç bir mekanik olurdu, ancak bu küçük oyun için amaçladığımız mekanik bu değil.
Şimdi sihirli blokların birbirine bağlı olup olmadığını (birbirlerinin solunda, sağında, üstünde veya altında bulunup bulunmadığını) algılayan bir mantığa ihtiyacımız var. Ayrıca tahtadaki tüm sihirli blokların birbirine bağlı olup olmadığını bilmemiz gerekiyor. Kullanılan algoritma oldukça basittir:
M listesi oluşturun.M listesindeki her blok için:
region değeri ayarlanmamışsa ona R bölge numarasını atayın (başlangıçta 1).R bölge numarasıyla işaretleyin ve onların komşularına, o komşuların komşularına ve bu şekilde devam edin.R bölge numarasını 1 artırın.
Algoritmanın uygulaması şöyledir:
-- board.script
--
-- Build list of all current magic blocks.
--
local function magic_blocks(self)
local magic = {}
for x = 0,boardwidth - 1 do
for y = 0,boardheight - 1 do
if self.board[x][y] ~= nil and self.board[x][y].color == hash("magic") then
table.insert(magic, self.board[x][y])
end
end
end
return magic
end
--
-- Filter out adjacent magic blocks
--
local function adjacent_magic_blocks(blocks, block)
return filter(function (e)
return (block.x == e.x and math.abs(block.y - e.y) == 1) or
(block.y == e.y and math.abs(block.x - e.x) == 1)
end, blocks)
end
--
-- Spread region to neighbors
--
local function mark_neighbors(blocks, block, region)
local neighbors = adjacent_magic_blocks(blocks, block)
for i, m in pairs(neighbors) do
if m.region == nil then
m.region = region
mark_neighbors(blocks, m, region)
end
end
end
--
-- Mark all magic block regions
--
local function mark_magic_regions(self)
local m_blocks = magic_blocks(self)
-- 1. Clear all region marks and count neighbors
for i, m in pairs(m_blocks) do
m.region = nil
local n = 0
for _ in pairs(adjacent_magic_blocks(m_blocks, m)) do n = n + 1 end
m.neighbors = n
end
-- 2. Assign regions and spread them
local region = 1
for i, m in pairs(m_blocks) do
if m.region == nil then
m.region = region
mark_neighbors(m_blocks, m, region)
region = region + 1
end
end
return m_blocks
end
Sihirli bloklar arasındaki bölgelerin sayısını hesaplamamızı sağlayan işlevler de oluşturuyoruz. Bölge sayısı 1 ise tüm sihirli blokların birbirine bağlı olduğunu biliriz. Ayrıca tüm sihirli blokların ışıklarını kapatan bir işlev ve komşusunda sihirli blok bulunan sihirli blokların ışık efektlerini açan bir işlev ekliyoruz:
-- board.script
--
-- Count the number of connected regions among the magic blocks.
--
local function count_magic_regions(blocks)
local maxr = 0
for i, m in pairs(blocks) do
if m.region > maxr then
maxr = m.region
end
end
return maxr
end
--
-- Shut off lights on all listed magic blocks
--
local function shutdown_lined_up_magic(self)
for i, m in ipairs(self.lined_up_magic) do
msg.post(m.id, "lights_off")
end
end
--
-- Set highlight for all magic blocks
--
local function highlight_magic(blocks)
for i, m in pairs(blocks) do
if m.neighbors > 0 then
msg.post(m.id, "lights_on")
else
msg.post(m.id, "lights_off")
end
end
end
Artık bu mantık parçalarını genel akışa ekleyebiliriz. Öncelikle tahta rastgele oluşturulduğu için küçük de olsa kazanılmış durumda başlama olasılığı vardır. Bu olursa tahtayı silip yeniden oluştururuz:
-- board.script
--
-- Clear the board
--
local function clear_board(self)
for y = 0,boardheight - 1 do
for x = 0,boardwidth - 1 do
if self.board[x][y] ~= nil then
go.delete(self.board[x][y].id)
self.board[x][y] = nil
end
end
end
end
local function build_board(self)
...
-- Build 1d list that we can easily filter.
build_blocklist(self)
local magic_blocks = mark_magic_regions(self)
if count_magic_regions(magic_blocks) == 1 then
-- "Win" from start. Make new board.
clear_board(self)
build_board(self)
end
highlight_magic(magic_blocks)
end
Mantığın geri kalanı on_input() içine sığar. level_completed iletisini işleyen kod hâlâ yok, ancak şimdilik bu sorun değil:
-- board.script
function on_input(self, action_id, action)
...
elseif action_id == hash("touch") and action.released then
-- Player released touch.
self.dragging = false
if #self.chain > 1 then
-- There is a chain of blocks. Remove it from board and refill board.
remove_chain(self)
slide_magic_blocks(self)
nilremoved(self)
-- Slide remaining blocks down.
slide_board(self)
local magic_blocks = mark_magic_regions(self)
-- Highlight adjacent magic blocks.
if count_magic_regions(magic_blocks) == 1 then
-- Win!
msg.post("#", "level_completed")
end
highlight_magic(magic_blocks)
end
self.chain = {}
-- Empty chain clears connector graphics.
for i, c in ipairs(self.connectors) do
go.delete(c)
end
self.connectors = {}
end
Tüm sihirli blokları bağladığınızda henüz hiçbir şey olmasa da artık oyunu oynamak ve kazanma durumuna ulaşmak mümkün.

“Blok bırakma” (drop) fikrinin amacı basit bir ilerleme mekaniği eklemektir. Oyuncu DROP düğmesine basarak sınırlı sayıda “blok bırakma” işlemi yapabilir; bu işlem tahtaya yukarıdan birkaç yeni rastgele parça bırakır. Oyuncu bir blok bırakma hakkıyla başlar ve her bölüm tamamlandığında ek bir hak kazanır. Blok bırakma mekaniğinin kodu iki işleve sığar. Biri blokların yerleşebileceği olası konumların listesini döndürür; diğeri ise animasyonu ve diğer ayrıntılarıyla blok bırakma işlemini gerçekleştirir.
-- board.script
--
-- Find spots for a drop.
--
local function dropspots(self)
local spots = {}
for x = 0, boardwidth - 1 do
for y = 0, boardheight - 1 do
if self.board[x][y] == nil then
table.insert(spots, { x = x, y = y })
break
end
end
end
-- If more than dropamount, randomly remove a slot until dropamount
for c = 1, #spots - dropamount do
table.remove(spots, math.random(#spots))
end
return spots
end
--
-- Perform the drop
--
local function drop(self, spots)
for i, s in pairs(spots) do
local pos = vmath.vector3()
pos.x = edge + blocksize / 2 + blocksize * s.x
pos.y = 1000
c = colors[math.random(#colors)] -- Pick a random color
local id = factory.create("#blockfactory", pos, null, { color = c })
go.animate(id, "position.y", go.PLAYBACK_ONCE_FORWARD, bottom_edge + blocksize / 2 + blocksize * s.y, go.EASING_OUTBOUNCE, 0.5)
-- Calc new z
go.set(id, "position.z", s.x * -0.1 + s.y * 0.01)
self.board[s.x][s.y] = { id = id, color = c, x = s.x, y = s.y }
end
-- Rebuild blocklist
build_blocklist(self)
end
Aşağıdaki kodu örneğin on_reload() içinde çalıştırarak veya geçici bir girdi eylemine bağlayarak blok bırakmayı test edebiliriz:
s = dropspots(self)
if #s > 0 then
-- Do the drop
drop(self, s)
end

Şimdi her şeyi bir araya getirme zamanı. Öncelikle bir başlangıç ekranı oluşturup bunu tahtadan ayıralım. İlk adım, bir main_menu.gui dosyası oluşturup içine bir Start düğmesi (bir metin düğümü ve dokulu bir kutu düğümü), bir başlık metni düğümü ve birkaç dekoratif blok (dokulu kutu düğümleri) yerleştirmektir. GUI’ye bağladığımız main_menu.gui_script betiği, init() içinde dekoratif bloklara animasyon uygular. Ayrıca ana betiğe start_game iletisi gönderen bir on_input() işlevi içerir. O betiği birazdan oluşturacağız.

-- main_menu.gui_script
function init(self)
msg.post(".", "acquire_input_focus")
local bs = { "brick1", "brick2", "brick3", "brick4", "brick5", "brick6" }
for i, b in ipairs(bs) do
local n = gui.get_node(b)
local rt = (math.random() * 3) + 1
local a = math.random(-45, 45)
gui.set_color(n, vmath.vector4(1, 1, 1, 0))
gui.animate(n, "position.y", -100 - math.random(0, 50), gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
gui.animate(n, "color.w", 1, gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
gui.animate(n, "rotation.z", a, gui.EASING_INSINE, 1 + rt, 0, nil, gui.PLAYBACK_LOOP_FORWARD)
end
gui.animate(gui.get_node("start"), "color.x", 1, gui.EASING_INOUTSINE, 1, 0, nil, gui.PLAYBACK_LOOP_PINGPONG)
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local start = gui.get_node("start")
if gui.pick_node(start, action.x, action.y) then
msg.post("/main#script", "start_game")
end
end
end
Oyunu başlatma görevini yakında ana menü betiği üstleneceği için board.script içindeki init() işlevinden geçici tahta hazırlama çağrısını kaldırın:
-- board.script
--
-- INIT
--
function init(self)
self.board = {} -- Contains the board structure
self.blocks = {} -- List of all blocks. Used for easy filtering on selection.
self.chain = {} -- Current selection chain
self.connectors = {} -- Connector elements to mark the selection chain
self.num_magic = 3 -- Number of magic blocks on the board
self.drops = 1 -- Number of drops you have available
self.magic_blocks = {} -- Magic blocks that are lined up
self.dragging = false -- Drag touch input
end
Ana betik oyunun genel durumunu tutacak ve istendiğinde oyunu başlatacak. Burada main.collection dosyasının, başlangıçta göstermemiz gereken en az miktarda varlığı içermesini istiyoruz. Bunu, main.collection içine ana menü GUI’sini, bir betik bileşenini ve en önemlisi bir Collection Proxy bileşenini barındıran “main” oyun nesnesini koyarak yapıyoruz.
Koleksiyon vekili (collection proxy), çalışan oyuna koleksiyonları dinamik olarak yüklememizi ve bunları bellekten kaldırmamızı sağlar. Belirtilen bir koleksiyon dosyası adına hareket eder; vekile ileti göndererek dinamik koleksiyonu yükler, başlangıç işlemlerini yapar, etkinleştirir, devre dışı bırakır ve bellekten kaldırırız. Koleksiyon vekillerinin kullanımına ilişkin eksiksiz açıklama için koleksiyon vekili belgelerine bakın.
Bu örnekte koleksiyon vekili bileşeninin Collection özelliğini, “bölümü” içeren board.collection dosyasına ayarlıyoruz.

Şimdi game.project dosyasını açıp başlangıç koleksiyonunu (bootstrap collection) belirleyen main_collection ayarını /main/main.collectionc olarak değiştirmeliyiz.

Artık oyunu başlatmak, tahtayı yüklemesi, başlangıç işlemlerini yapması ve etkinleştirmesi için koleksiyon vekilimize ileti göndermek, ardından ana menüyü (görünmemesi için) devre dışı bırakmak anlamına gelir. Ana menüye dönmek ise bu işlemleri tersine çevirir (vekilin koleksiyonu yüklemiş olması koşuluyla).
-- main.script
function init(self)
msg.post("#", "to_main_menu")
self.state = "MAIN_MENU"
end
function on_message(self, message_id, message, sender)
if message_id == hash("to_main_menu") then
if self.state ~= "MAIN_MENU" then
msg.post("#boardproxy", "unload")
end
msg.post("main:/main#menu", "enable") -- <1>
self.state = "MAIN_MENU"
elseif message_id == hash("start_game") then
msg.post("#boardproxy", "load")
msg.post("#menu", "disable")
elseif message_id == hash("proxy_loaded") then
-- Board collection has loaded...
msg.post(sender, "init")
msg.post("board:/board#script", "start_level", { difficulty = 1 }) -- <2>
msg.post(sender, "enable")
self.state = "GAME_RUNNING"
end
end
Tahta betiğine mantığın son parçasını eklemeden önce tahtaya bir dizi GUI öğesi eklemeliyiz. Önce tahtanın üst kısmına bir RESTART düğmesi ve bir DROP düğmesi ekliyoruz.

Tahta GUI’sinin betiği, yeniden başlatma düğmesine tıklandığında yeniden başlatma GUI iletişim kutusuna, DROP düğmesine tıklandığında ise tahta betiğinin kendisine ileti gönderir:
-- board.gui_script
function init(self)
msg.post("#", "show")
msg.post("/restart#gui", "hide")
msg.post("/level_complete#gui", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
msg.post("#", "enable")
elseif message_id == hash("set_drop_counter") then
local n = gui.get_node("drop_counter")
gui.set_text(n, message.drops .. " x")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local restart = gui.get_node("restart")
local drop = gui.get_node("drop")
if gui.pick_node(restart, action.x, action.y) then
-- Show the restart dialog box.
msg.post("/restart#gui", "show")
msg.post("#", "hide")
elseif gui.pick_node(drop, action.x, action.y) then
msg.post("/board#script", "drop")
end
end
end
RESTART iletişim kutusu basittir. Bunu restart.gui olarak oluşturup oyuncu NO seçeneğine tıkladığında hiçbir şey yapmayan, YES seçeneğine tıkladığında tahta betiğine restart_level iletisi ve Quit to main menu seçeneğine tıkladığında ana betiğe to_main_menu iletisi gönderen basit bir betik ekliyoruz:

-- restart.gui_script
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
msg.post(".", "release_input_focus")
elseif message_id == hash("show") then
msg.post("#", "enable")
msg.post(".", "acquire_input_focus")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local yes = gui.get_node("yes")
local no = gui.get_node("no")
local quit = gui.get_node("quit")
if gui.pick_node(no, action.x, action.y) then
msg.post("#", "hide")
msg.post("/board#gui", "show")
elseif gui.pick_node(yes, action.x, action.y) then
msg.post("board:/board#script", "restart_level")
msg.post("/board#gui", "show")
msg.post("#", "hide")
elseif gui.pick_node(quit, action.x, action.y) then
msg.post("main:/main#script", "to_main_menu")
msg.post("#", "hide")
end
end
-- Consume all input until we're gone.
return true
end
Ayrıca level_complete.gui içinde bölümün tamamlanması için basit bir GUI iletişim kutusu oluşturuyor ve oyuncu CONTINUE seçeneğine tıkladığında tahta betiğine next_level iletisi gönderen basit bir betik ekliyoruz:

-- level_complete.gui_script
function init(self)
msg.post("#", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
msg.post(".", "release_input_focus")
elseif message_id == hash("show") then
msg.post("#", "enable")
msg.post(".", "acquire_input_focus")
end
end
function on_input(self, action_id, action)
if action_id == hash("touch") and action.pressed then
local continue = gui.get_node("continue")
if gui.pick_node(continue, action.x, action.y) then
msg.post("board#script", "next_level")
msg.post("#", "hide")
end
end
-- Consume all input until we're gone.
return true
end
Geçerli bölümü tanıtmak için kullanılan bir iletişim kutusu ve yalnızca kutuyu gizleme ile gösterme işlemlerini içeren bir betik oluşturuyoruz. Gösterildiğinde iletişim kutusunun metni, geçerli zorluk düzeyini içerecek şekilde ayarlanır:

-- present_level.gui_script
function init(self)
msg.post("#", "hide")
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
local n = gui.get_node("message")
gui.set_text(n, "Level " .. message.level)
msg.post("#", "enable")
end
end
Oyuncu blok bırakmaya çalıştığında buna yer yoksa gösterilen bir iletişim kutusu da ekliyoruz.

-- no_drop_room.gui_script
function init(self)
msg.post("#", "hide")
self.t = 0
end
function update(self, dt)
if self.t < 0 then
msg.post("#", "hide")
else
self.t = self.t - dt
end
end
function on_message(self, message_id, message, sender)
if message_id == hash("hide") then
msg.post("#", "disable")
elseif message_id == hash("show") then
self.t = 1
msg.post("#", "enable")
end
end
Son olarak bu GUI bileşenlerini board.collection içine ve gerekli kodu board.script içine ekliyoruz:

Tahtaya gönderilen ve tahtadan gelen tüm iletiler için on_message() içinde kod yazmamız gerekiyor.
start_levelgo.animate() işlevini zamanlayıcı olarak kullandığımıza dikkat edin.restart_levellevel_completednext_level iletisi gönderecektir.next_levelstart_level iletisini gönderin.drop-- board.script
function on_message(self, message_id, message, sender)
if message_id == hash("start_level") then
self.num_magic = message.difficulty + 1
build_board(self)
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
msg.post("present_level#gui", "show", { level = message.difficulty } )
-- Wait some...
go.animate("#", "timer", go.PLAYBACK_ONCE_FORWARD, 1, go.EASING_LINEAR, 2, 0, function ()
msg.post("present_level#gui", "hide")
msg.post(".", "acquire_input_focus")
end)
elseif message_id == hash("restart_level") then
clear_board(self)
build_board(self)
self.drops = 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
msg.post(".", "acquire_input_focus")
elseif message_id == hash("level_completed") then
-- turn off input
msg.post(".", "release_input_focus")
-- Animate the magic!
for i, m in ipairs(magic_blocks(self)) do
go.set_scale_xy(0.17, m.id)
go.animate(m.id, "scale.xy", go.PLAYBACK_LOOP_PINGPONG, 0.19, go.EASING_INSINE, 0.5, 0)
end
-- Show completion screen
msg.post("level_complete#gui", "show")
elseif message_id == hash("next_level") then
clear_board(self)
self.drops = self.drops + 1
-- Difficulty level is number of magic blocks - 1
msg.post("#", "start_level", { difficulty = self.num_magic })
elseif message_id == hash("drop") then
s = dropspots(self)
if #s == 0 then
-- Can't perform drop
msg.post("no_drop_room#gui", "show")
elseif self.drops > 0 then
-- Do the drop
drop(self, s)
self.drops = self.drops - 1
msg.post("#gui", "set_drop_counter", { drops = self.drops } )
end
end
end
İşte bu kadar! Oyun ve bu öğretici artık tamamlandı! Oyunun tadını çıkarın!

Bu küçük oyunun bazı ilginç özellikleri var ve üzerinde denemeler yapmanızı öneririz. Defold’a daha fazla alışmak için yapabileceğiniz alıştırmaların listesi şöyledir: