A Match 3 game template.
Gameplay
MatchSweetsGameplay.mp4
A Match 3 game template with three implementations to fill the playing field. Use this project as a starting point for creating your own Match 3 game.
Simple Fill Strategy | Fall Down Fill Strategy | Slide Down Fill Strategy |
Note: The
FallDownFillStrategy
&SlideDownFillStrategy
are given as an example. Consider to implement an object pooling technique for theItemMoveData
to reduce memory pressure.
.
βββ Art
β βββ Icons
β β βββ Food
β β βββ Sweets
β βββ Sprites
β βββ Textures
β
βββ Plugins
β βββ Match3
β βββ App
β β βββ Internal
β β β βββ ...
β β β βββ GameBoard.cs
β β β βββ JobsExecutor.cs
β β βββ ...
β β βββ GameConfig.cs
β β βββ Job.cs
β β βββ LevelGoal.cs
β β βββ Match3Game.cs
β βββ Core
β
βββ Prefabs
β βββ ItemPrefab.prefab
β βββ TilePrefab.prefab
β
βββ Scenes
β βββ MainScene.unity
β
βββ Scripts
β βββ Common
β β βββ AppModes
β β β βββ DrawGameBoardMode.cs
β β β βββ GameInitMode.cs
β β β βββ GamePlayMode.cs
β β βββ LevelGoals
β β βββ SequenceDetectors
β β βββ ...
β β βββ GameBoardRenderer.cs
β β βββ GameBoardSolver.cs
β β βββ ItemGenerator.cs
β β βββ LevelGoalsProvider.cs
β β βββ UnityItem.cs
β βββ FillStrategies
β β βββ Jobs
β β βββ Models
β β βββ FallDownFillStrategy.cs
β β βββ SimpleFillStrategy.cs
β β βββ SlideDownFillStrategy.cs
β βββ App.cs
β βββ AppContext.cs
β βββ Game.cs
To add a new icons set, simply create a SpriteAtlas
and add it to the AppContext
via the Inspector.
Note: You can change icons size by changing the
Pixels Per Unit
option in the sprite settings.
Let's create a SlideIn
animation to show the items and a SlideOut
animation to hide the items. These animations will be used further.
Π‘reate a class ItemsSlideOutJob
and inherit from the Job
.
public class ItemsSlideOutJob : Job
{
private const float FadeDuration = 0.15f;
private const float SlideDuration = 0.2f;
private readonly IEnumerable<IUnityItem> _items;
public ItemsSlideOutJob(IEnumerable<IUnityItem> items, int executionOrder = 0) : base(executionOrder)
{
_items = items; // Items to animate.
}
public override async UniTask ExecuteAsync()
{
var itemsSequence = DOTween.Sequence();
foreach (var item in _items)
{
// Calculate the item destination position.
var destinationPosition = item.GetWorldPosition() + Vector3.right;
_ = itemsSequence
.Join(item.Transform.DOMove(destinationPosition, SlideDuration)) // Smoothly move the item.
.Join(item.SpriteRenderer.DOFade(0, FadeDuration)); // Smoothly hide the item.
}
await itemsSequence.SetEase(Ease.Flash);
}
}
Then create a class ItemsSlideInJob
.
public class ItemsSlideInJob : Job
{
private const float FadeDuration = 0.15f;
private const float SlideDuration = 0.2f;
private readonly IEnumerable<IUnityItem> _items;
public ItemsSlideInJob(IEnumerable<IUnityItem> items, int executionOrder = 0) : base(executionOrder)
{
_items = items; // Items to animate.
}
public override async UniTask ExecuteAsync()
{
var itemsSequence = DOTween.Sequence();
foreach (var item in _items)
{
// Save the item current position.
var destinationPosition = item.GetWorldPosition();
// Move the item to the starting position.
item.SetWorldPosition(destinationPosition + Vector3.left);
// Reset the item scale.
item.Transform.localScale = Vector3.one;
// Reset the sprite alpha to zero.
item.SpriteRenderer.SetAlpha(0);
// Activate the item game object.
item.Show();
_ = itemsSequence
.Join(item.Transform.DOMove(destinationPosition, SlideDuration)) // Smoothly move the item.
.Join(item.SpriteRenderer.DOFade(1, FadeDuration)); // Smoothly show the item.
}
await itemsSequence.SetEase(Ease.Flash);
}
}
Jobs with the same executionOrder
run in parallel. Otherwise, they run one after the other according to the executionOrder
.
First of all, create a class SidewayFillStrategy
and inherit from the IBoardFillStrategy<TItem>
.
We'll need an IGameBoardRenderer
to transform grid positions to world positions and an IItemsPool<TItem>
to get the pre-created items from the pool. Let's pass them to the constructor.
public class SidewayFillStrategy : IBoardFillStrategy<IUnityItem>
{
private readonly IGameBoardRenderer _gameBoardRenderer;
private readonly IItemsPool<IUnityItem> _itemsPool;
public SidewayFillStrategy(IGameBoardRenderer gameBoardRenderer, IItemsPool<IUnityItem> itemsPool)
{
_itemsPool = itemsPool;
_gameBoardRenderer = gameBoardRenderer;
}
public string Name => "Sideway Fill Strategy";
public IEnumerable<IJob> GetFillJobs(IGameBoard<IUnityItem> gameBoard)
{
throw new NotImplementedException();
}
public IEnumerable<IJob> GetSolveJobs(IGameBoard<IUnityItem> gameBoard,
IEnumerable<ItemSequence<IUnityItem>> sequences)
{
throw new NotImplementedException();
}
}
Then let's implement the GetFillJobs
method. This method is used to fill the playing field.
public IEnumerable<IJob> GetFillJobs(IGameBoard<IUnityItem> gameBoard)
{
// List of items to show.
var itemsToShow = new List<IUnityItem>();
for (var rowIndex = 0; rowIndex < gameBoard.RowCount; rowIndex++)
{
for (var columnIndex = 0; columnIndex < gameBoard.ColumnCount; columnIndex++)
{
var gridSlot = gameBoard[rowIndex, columnIndex];
if (gridSlot.State != GridSlotState.Empty)
{
continue;
}
// Get an item from the pool.
var item = _itemsPool.GetItem();
// Set the position of the item.
item.SetWorldPosition(_gameBoardRenderer.GetWorldPosition(rowIndex, columnIndex));
// Set the item to the grid slot.
gridSlot.SetItem(item);
// Add the item to the list to show.
itemsToShow.Add(item);
}
}
// Create a job to show items.
return new[] { new ItemsShowJob(itemsToShow) };
}
Next, we implement the GetSolveJobs
method. This method is used to deal with solved sequences of items.
public IEnumerable<IJob> GetSolveJobs(IGameBoard<IUnityItem> gameBoard,
IEnumerable<ItemSequence<IUnityItem>> sequences)
{
// List of items to hide.
var itemsToHide = new List<IUnityItem>();
// List of items to show.
var itemsToShow = new List<IUnityItem>();
foreach (var solvedGridSlot in sequences.GetUniqueGridSlots())
{
// Get a new item from the pool.
var newItem = _itemsPool.GetItem();
// Get the current item of the grid slot.
var currentItem = solvedGridSlot.Item;
// Set the position of the new item.
newItem.SetWorldPosition(currentItem.GetWorldPosition());
// Set the new item to the grid slot.
solvedGridSlot.SetItem(newItem);
// Add the current item to the list to hide.
itemsToHide.Add(currentItem);
// Add the new item to the list to show.
itemsToShow.Add(newItem);
// Return the current item to the pool.
_itemsPool.ReturnItem(currentItem);
}
// Create jobs to hide and show items using the animations we created above.
return new IJob[] { new ItemsSlideOutJob(itemsToHide), new ItemsSlideInJob(itemsToShow) };
}
Once the SidewayFillStrategy
is implemented. Register it in the AppContext
class.
public class AppContext : MonoBehaviour, IAppContext
{
...
private IBoardFillStrategy<IUnityItem>[] GetBoardFillStrategies(IGameBoardRenderer gameBoardRenderer,
IItemsPool<IUnityItem> itemsPool)
{
return new IBoardFillStrategy<IUnityItem>[]
{
...
new SidewayFillStrategy(gameBoardRenderer, itemsPool)
};
}
...
}
Video Demonstration
ItemsSlideFillStrategy.mp4
Let's say we want to add a goal to collect a certain number of specific items. First of all, create a class CollectItems
and inherit from the LevelGoal<TItem>
.
public class CollectItems : LevelGoal<IUnityItem>
{
private readonly int _contentId;
private readonly int _itemsCount;
private int _collectedItemsCount;
public CollectItems(int contentId, int itemsCount)
{
_contentId = contentId;
_itemsCount = itemsCount;
}
public override void RegisterSolvedSequences(IEnumerable<ItemSequence<IUnityItem>> sequences)
{
foreach (var solvedGridSlot in sequences.GetUniqueGridSlots())
{
if (solvedGridSlot.Item.ContentId == _contentId)
{
_collectedItemsCount++;
}
}
if (_collectedItemsCount >= _itemsCount)
{
MarkAchieved();
}
}
}
Once the level goal is implemented. Don't forget to register it in the LevelGoalsProvider
.
public class LevelGoalsProvider : ILevelGoalsProvider<IUnityItem>
{
public LevelGoal<IUnityItem>[] GetLevelGoals(int level, IGameBoard<IUnityItem> gameBoard)
{
return new LevelGoal<IUnityItem>[]
{
...
new CollectItems(0, 25)
};
}
}
Let's implement a new sequence detector to detect square shapes. Create a class SquareShapeDetector
and inherit from the ISequenceDetector<TItem>
.
First of all, we have to declare an array of lookup directions.
public class SquareShapeDetector : ISequenceDetector<IUnityItem>
{
private readonly GridPosition[][] _squareLookupDirections;
public SquareShapeDetector()
{
_squareLookupDirections = new[]
{
new[] { GridPosition.Up, GridPosition.Left, GridPosition.Up + GridPosition.Left },
new[] { GridPosition.Up, GridPosition.Right, GridPosition.Up + GridPosition.Right },
new[] { GridPosition.Down, GridPosition.Left, GridPosition.Down + GridPosition.Left },
new[] { GridPosition.Down, GridPosition.Right, GridPosition.Down + GridPosition.Right },
};
}
public ItemSequence<IUnityItem> GetSequence(IGameBoard<IUnityItem> gameBoard, GridPosition gridPosition)
{
throw new NotImplementedException();
}
}
Then let's implement the GetSequence
method.
public ItemSequence<IUnityItem> GetSequence(IGameBoard<IUnityItem> gameBoard, GridPosition gridPosition)
{
var sampleGridSlot = gameBoard[gridPosition];
var gridSlots = new List<GridSlot<IUnityItem>>(4);
foreach (var lookupDirections in _squareLookupDirections)
{
foreach (var lookupDirection in lookupDirections)
{
var position = gridPosition + lookupDirection;
if (gameBoard.IsPositionOnBoard(position) == false)
{
break;
}
var gridSlot = gameBoard[position];
if (gridSlot.Item == null)
{
break;
}
if (gridSlot.Item.ContentId == sampleGridSlot.Item.ContentId)
{
gridSlots.Add(gridSlot);
}
}
if (gridSlots.Count == 3)
{
gridSlots.Add(sampleGridSlot);
break;
}
gridSlots.Clear();
}
return gridSlots.Count > 0 ? new ItemSequence<IUnityItem>(GetType(), gridSlots) : null;
}
Finally, add the SquareShapeDetector
to the sequence detector list of the GameBoardSolver
constructor in the AppContext
class.
public class AppContext : MonoBehaviour, IAppContext
{
...
private IGameBoardSolver<IUnityItem> GetGameBoardSolver()
{
return new GameBoardSolver(new ISequenceDetector<IUnityItem>[]
{
...
new SquareShapeDetector()
});
}
...
}
Here are some features which are either under way or planned:
- Add tests
- Build .unitypackage
- Publish on Asset Store
- Optimize
ItemsDrop
&ItemsRollDown
fill strategies
You may contribute in several ways like creating new features, fixing bugs or improving documentation and examples. Find more information in CONTRIBUTING.md.
If you find a bug in the source code, please create bug report.
Please browse existing issues to see whether a bug has previously been reported.
If you have an idea, or you're missing a capability that would make development easier, please submit feature request.
If a similar feature request already exists, don't forget to leave a "+1" or add additional information, such as your thoughts and vision about the feature.
Give a β if this project helped you!
Usage is provided under the MIT License.