Dynamine is a match-3 game where you explore a dangerous mine shaft in search for precious ore and gems. Your goal is to top your high score and collect the ore to upgrade your equipment for your next attempt. Tap your way to success and see how far you can get.
The game was developed in 3 weeks with no prior knowledge of LUA or Defold.
Platform: Android/IOS
Tools Used: Defold
Duration: 3 Weeks
Team Size: 4
Role: Scripter
Responsibilities: Gameplay Scripting, System Scripting
The game spawns blocks in 4 columns within an interval based on how far into the game it is. The player has to tap each block to break them. As a block is destroyed, that row will fall down as if it was affected by gravity.
- function SpawnBlocks(self)
- local filledrows = boardWidth
- local tries = 0
- while filledrows > 0 and tries < boardWidth * 5 do
- tries = tries + 1
- for x = 0, boardWidth - 1 do
- if filledrows <= 0 then
- return
- end
- -- Find which Y value the block is going to have
- local y = FindLowestEmptySlot(self, x)
- local pos = go.get_world_position() + vmath.vector3(leftEdge + x * blockSize + offset,bottomEdge + y * blockSize + offset + 500, 0)
- if self.board[x][y] == nil then
- local shouldPlace = false
- local min
- local ore
- -- This loop manipulated the RNG of the blocks so that
- -- ores doesnt spawn multiple in a row
- repeat
- -- Pick a random sprite for the block
- min = DifficultyList[level + math.random(levelScale)]
- -- Does the block have a ore inside of it?
- if math.random(100) < oreSpawnChance then
- theore = RandomOre(self)
- ore = ores[RandomOre(self)]
- else
- ore = nil
- end
- shouldPlace, min, ore = ManipulateRNG(self, min, ore, x, y)
- until shouldPlace
- local mineralOre
- if ore == nil then
- mineralOre = hash("none")
- else
- mineralOre = ore.tile
- end
- if math.random() < gemTile.gemchance then
- min = gemTile
- mineralOre = hash("none")
- end
- -- Set the visuals for the tile to the first in its list of sprites
- -- This is used for when blocks are cracked open slowly
- local starttile = (min.tile[1])
- -- Create a new block
- local obj = factory.create("#blockFactory", pos, null, { mineral = starttile , health = min.health, ore = mineralOre })
- local objectURL = msg.url(nil, obj, "sprite")
- -- Send the sprite information to the newly created block
- msg.post(obj, "SetSprites", {sprites = min.tile})
- -- Set the scale so that it fits the columns regardless of sprite resolution
- local scale = blockSize / go.get(objectURL, "size").x
- go.set_scale(vmath.vector3(scale, scale, scale), obj)
- -- Assign the block in the matrix of blocks
- self.board[x][y] = { id = obj, mineral = min, ore = ore, x = x, y = y , worldLocation = pos, moving = true}
- filledrows = filledrows - 1
- MoveBlocksDownRow(self , x)
- end
- end
- end
- end
The game manipulates the blocks to always have variety by checking the block below. If the block below is the same, then the manipulated block is changed into another block.
Without Manipulation
With Manipulation
If the player matches 3 or 4 blocks in a row, then they will all be destroyed. The player gains more score and resources the more blocks that are matched.
- local function CheckForLine(self)
- -- Go through the entire row of blocks
- for i = 0, boardWidth - 1 do
- -- Count the amount of the same block in the row
- -- This counts the blocks after the current block (i)
- local chain = CheckEntireRow(self, i)
- -- If the amount is more than a threshold
- if #chain >= minimumMatchRows then
- local destroyRow = true
- -- Check if none of the tiles in the row is moving
- -- If they are standing still, destroy them
- -- Otherwise do nothing
- for q = 1, #chain do
- if self.board[chain[q].x][chain[q].y].moving then
- destroyRow = false
- break
- end
- end
- -- If we destroy the row, give score and remove from the matrix of blocks
- if destroyRow then
- for j = 1, #chain do
- msg.post(self.board[chain[j].x][chain[j].y].id, "DamageAnimation")
- local xvalue = self.board[chain[j].x][chain[j].y].x
- local yvalue = self.board[chain[j].x][chain[j].y].y
- -- Add a base score of 10
- local score = 10
- gamescore = gamescore + score
- -- Give materials to the player
- -- It also calculate if it should or should not give materials
- GiveOreToPlayer(self, self.board[chain[j].x][chain[j].y].ore, 2)
- -- Do the same with gems (best currency)
- ChanceToGiveGems(self, chain)
- OnBlockDestroyed(self)
- -- Count the total amount of blocks destroyed
- Achievements["Blocks"] = Achievements["Blocks"] + 1
- -- If we have ores, create an ore object in the world
- -- at the location of the block
- if self.board[chain[j].x][chain[j].y].ore ~= nil then
- VisualizeOres(self, self.board[chain[j].x][chain[j].y])
- end
- -- Remove the block from the matrix
- go.delete(self.board[chain[j].x][chain[j].y].id)
- self.board[chain[j].x][chain[j].y] = nil
- -- Move down the row
- MoveBlocksDownRow(self , xvalue)
- msg.post("GUI#gui", "UpdateScore", {score = score, x = xvalue, y = yvalue})
- -- Add value to the on fire system
- OnFire(self, 2)
- end
- -- Count some achievements depending on if its
- -- a line of 3 or 4
- if #chain == 3 then
- Achievements["3-row"] = Achievements["3-row"] + 1
- elseif #chain == 4 then
- Achievements["4-row"] = Achievements["4-row"] + 1
- end
- ShakeCamera(self, 0.1)
- end
- end
- end
- end
Since the game is quite small in terms of mechanics, we needed to make the game feel good. The second to second gameplay is tapping blocks which means that it needs to feel good to tap them.
To make the tapping feel better, I added a small animation to the blocks and a camera shake if the player matches 3 or 4 in a row. I also implemented some particles every time you tap on a block to add another small detail.
- -- If we should shake the block or not
- if self.shaketime > 0 then
- -- Find a random rotation
- local rot = vmath.quat_rotation_z(math.rad(math.random(-shakestrength, shakestrength)))
- local currentRotation = go.get_rotation()
- -- Lerp between the current rotation and the randomed rotation
- local lrot = vmath.lerp(smoothness * dt, currentRotation, rot)
- go.set_rotation(lrot)
- self.shaketime = self.shaketime - dt
- else
- go.set_rotation(self.startRotation)
- end
If the player loses the game by filling the entire board, the game will save statistics of how well he or she performed. If it is better than previous, then that will be a new record. The game saves amount of blocks destroyed, number of rows matched, score and highest stage. This feature reinforces the retention of the player.
As the player gather resources, he or she can craft additional pickaxes. These pickaxes allows the player to progress further into the game by being stronger. Each pickaxe has a durability which is broken after a certain amount of taps.