- Notifications
You must be signed in to change notification settings - Fork97
SDK for creating Match 3 games
License
LibraStack/Match3-SDK
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A cross-platform library that makes it easy to create your own Match 3 game.
TheMatch 3 SDK is designed to speed up the development of Match 3 games. Use the samples as a starting point for creating your own Match 3 game.
A Match 3 game sample with three implementations to fill the playing field.
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.
Gameplay Demonstration
UnityMatch3Gameplay.mp4
A Match 3 game sample designed for text terminals.
Gameplay Demonstration
TerminalMatch3Gameplay.mp4
Note: The sample was tested using Rider's internal console. If you have a problem displaying symbols, configure your terminal to supportUnicode (inUTF-8 form). For Windows, you can use the newWindows Terminal.
.├── samples│ ├── Terminal.Match3│ └── Unity.Match3│├── src│ ├── Match3.App│ ├── Match3.Core│ ├── Match3.Template│ └── Match3.UnityPackage # Auto-generated│├── Match3.sln
Dependencies:
- Match3-SDK:UniTask
- Unity Match3-SDK:UniTask
- Unity Match3-Sample:UniTask,DOTween &Match3-SDK
You can install Match3-SDK in one of the following ways:
1. Install via Package Manager
The package is available on theOpenUPM.
Open
Edit/Project Settings/Package Manager
Add a new
Scoped Registry
(or edit the existing OpenUPM entry)Name package.openupm.comURL https://package.openupm.comScope(s) com.cysharp.unitask com.chebanovdd.match3sdk
Open
Window/Package Manager
Select
My Registries
Install
UniTask
andMatch3-SDK
packages
2. Install via Git URL
You can addhttps://github.com/ChebanovDD/Match3-SDK.git?path=src/Match3.UnityPackage/Assets/Plugins/Match3
to the Package Manager.
If you want to set a target version, Match3-SDK uses thev*.*.*
release tag, so you can specify a version like#v0.1.2
. For examplehttps://github.com/ChebanovDD/Match3-SDK.git?path=src/Match3.UnityPackage/Assets/Plugins/Match3#v0.1.2
.
Note: Dependencies must be installed before installing the package.
- Match3.SDK.zip - to use the Match3-SDK outside of Unity (eg. just as a normal C# project)
- Match3.Unity.SDK.unitypackage - contains Match3-SDK source code
- Match3.Unity.Sample.unitypackage - contains the sample project for Unity
- com.chebanovdd.match3sdk-*.tgz - for installing the Match3-SDKfrom a local tarball file
Note: Dependencies must be installed before installing the packages.
To add a new icons set, simply create aSpriteAtlas
and add it to theAppContext
via the Inspector.
Note: You can change icons size by changing the
Pixels Per Unit
option in the sprite settings.
Let's create aSlideIn
animation to show the items and aSlideOut
animation to hide the items. These animations will be used further.
Сreate a classItemsSlideOutJob
and inherit from theJob
.
publicclassItemsSlideOutJob:Job{privateconstfloatFadeDuration=0.15f;privateconstfloatSlideDuration=0.2f;privatereadonlyIEnumerable<IUnityItem>_items;publicItemsSlideOutJob(IEnumerable<IUnityItem>items,intexecutionOrder=0):base(executionOrder){_items=items;// Items to animate.}publicoverrideasyncUniTaskExecuteAsync(CancellationTokencancellationToken=default){varitemsSequence=DOTween.Sequence();foreach(varitemin_items){// Calculate the item destination position.vardestinationPosition=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.}awaititemsSequence.SetEase(Ease.Flash).WithCancellation(cancellationToken);}}
Then create a classItemsSlideInJob
.
publicclassItemsSlideInJob:Job{privateconstfloatFadeDuration=0.15f;privateconstfloatSlideDuration=0.2f;privatereadonlyIEnumerable<IUnityItem>_items;publicItemsSlideInJob(IEnumerable<IUnityItem>items,intexecutionOrder=0):base(executionOrder){_items=items;// Items to animate.}publicoverrideasyncUniTaskExecuteAsync(CancellationTokencancellationToken=default){varitemsSequence=DOTween.Sequence();foreach(varitemin_items){// Save the item current position.vardestinationPosition=item.GetWorldPosition();// Move the item to the starting position.item.SetWorldPosition(destinationPosition+Vector3.left);// Reset the sprite alpha to zero.item.SpriteRenderer.SetAlpha(0);// Reset the item scale.item.SetScale(1);// 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.}awaititemsSequence.SetEase(Ease.Flash).WithCancellation(cancellationToken);}}
Jobs with the sameexecutionOrder
run in parallel. Otherwise, they run one after the other according to theexecutionOrder
.
First of all, create a classSidewayFillStrategy
and inherit from theIBoardFillStrategy<TGridSlot>
.
We'll need anIUnityGameBoardRenderer
to transform grid positions to world positions and anIItemsPool<TItem>
to get the pre-created items from the pool. Let's pass them to the constructor.
publicclassSidewayFillStrategy:IBoardFillStrategy<IUnityGridSlot>{privatereadonlyIItemsPool<IUnityItem>_itemsPool;privatereadonlyIUnityGameBoardRenderer_gameBoardRenderer;publicSidewayFillStrategy(IUnityGameBoardRenderergameBoardRenderer,IItemsPool<IUnityItem>itemsPool){_itemsPool=itemsPool;_gameBoardRenderer=gameBoardRenderer;}publicstringName=>"Sideway Fill Strategy";publicIEnumerable<IJob>GetFillJobs(IGameBoard<IUnityGridSlot>gameBoard){thrownewNotImplementedException();}publicIEnumerable<IJob>GetSolveJobs(IGameBoard<IUnityGridSlot>gameBoard,SolvedData<IUnityGridSlot>solvedData){thrownewNotImplementedException();}}
Then let's implement theGetFillJobs
method. This method is used to fill the playing field.
publicIEnumerable<IJob>GetFillJobs(IGameBoard<IUnityGridSlot>gameBoard){// List of items to show.varitemsToShow=newList<IUnityItem>();for(varrowIndex=0;rowIndex<gameBoard.RowCount;rowIndex++){for(varcolumnIndex=0;columnIndex<gameBoard.ColumnCount;columnIndex++){vargridSlot=gameBoard[rowIndex,columnIndex];if(gridSlot.CanSetItem==false){continue;}// Get an item from the pool.varitem=_itemsPool.GetItem();// Set the position of the item.item.SetWorldPosition(_gameBoardRenderer.GetWorldPosition(gridSlot.GridPosition));// 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.returnnew[]{newItemsShowJob(itemsToShow)};}
Next, we implement theGetSolveJobs
method. This method is used to deal with solved sequences of items.
publicIEnumerable<IJob>GetSolveJobs(IGameBoard<IUnityGridSlot>gameBoard,SolvedData<IUnityGridSlot>solvedData){// List of items to hide.varitemsToHide=newList<IUnityItem>();// List of items to show.varitemsToShow=newList<IUnityItem>();// Iterate through the solved items.// Get unique and only movable items.foreach(varsolvedGridSlotinsolvedData.GetUniqueSolvedGridSlots(true)){// Get a new item from the pool.varnewItem=_itemsPool.GetItem();// Get the current item of the grid slot.varcurrentItem=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);}// Iterate through the special items (can be empty).// Get all special items except occupied.foreach(varspecialItemGridSlotinsolvedData.GetSpecialItemGridSlots(true)){varitem=_itemsPool.GetItem();item.SetWorldPosition(_gameBoardRenderer.GetWorldPosition(specialItemGridSlot.GridPosition));specialItemGridSlot.SetItem(item);itemsToShow.Add(item);}// Create jobs to hide and show items using the animations we created above.returnnewIJob[]{newItemsSlideOutJob(itemsToHide),newItemsSlideInJob(itemsToShow)};}
Note: The
SolvedSequences
&SpecialItemGridSlots
can contain overlapping items.
Once theSidewayFillStrategy
is implemented. Register it in theAppContext
class.
publicclassAppContext:MonoBehaviour,IAppContext{ ...privateIBoardFillStrategy<IUnityGridSlot>[]GetBoardFillStrategies(IUnityGameBoardRenderergameBoardRenderer,IItemsPool<IUnityItem>itemsPool){returnnewIBoardFillStrategy<IUnityGridSlot>[]{ ... newSidewayFillStrategy(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 classCollectItems
and inherit from theLevelGoal<TGridSlot>
.
publicclassCollectItems:LevelGoal<IUnityGridSlot>{privatereadonlyint_contentId;privatereadonlyint_itemsCount;privateint_collectedItemsCount;publicCollectItems(intcontentId,intitemsCount){_contentId=contentId;_itemsCount=itemsCount;}publicoverridevoidOnSequencesSolved(SolvedData<IUnityGridSlot>solvedData){// Get unique and only movable items.foreach(varsolvedGridSlotinsolvedData.GetUniqueSolvedGridSlots(true)){if(solvedGridSlot.Item.ContentId==_contentId){_collectedItemsCount++;}}if(_collectedItemsCount>=_itemsCount){MarkAchieved();}}}
Once the level goal is implemented. Don't forget to register it in theLevelGoalsProvider
.
publicclassLevelGoalsProvider:ILevelGoalsProvider<IUnityGridSlot>{publicLevelGoal<IUnityGridSlot>[]GetLevelGoals(intlevel,IGameBoard<IUnityGridSlot>gameBoard){returnnewLevelGoal<IUnityGridSlot>[]{ ... newCollectItems(0,25)};}}
Let's implement a new sequence detector to detect square shapes. Create a classSquareShapeDetector
and inherit from theISequenceDetector<TGridSlot>
.
First of all, we have to declare an array of lookup directions.
publicclassSquareShapeDetector:ISequenceDetector<IUnityGridSlot>{privatereadonlyGridPosition[][]_squareLookupDirections;publicSquareShapeDetector(){_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},};}publicItemSequence<IUnityGridSlot>GetSequence(IGameBoard<IUnityGridSlot>gameBoard,GridPositiongridPosition){thrownewNotImplementedException();}}
Then let's implement theGetSequence
method.
publicItemSequence<IUnityGridSlot>GetSequence(IGameBoard<IUnityGridSlot>gameBoard,GridPositiongridPosition){varsampleGridSlot=gameBoard[gridPosition];varresultGridSlots=newList<IUnityGridSlot>(4);foreach(varlookupDirectionsin_squareLookupDirections){foreach(varlookupDirectioninlookupDirections){varlookupPosition=gridPosition+lookupDirection;if(gameBoard.IsPositionOnBoard(lookupPosition)==false){break;}varlookupGridSlot=gameBoard[lookupPosition];if(lookupGridSlot.HasItem==false){break;}if(lookupGridSlot.Item.ContentId==sampleGridSlot.Item.ContentId){resultGridSlots.Add(lookupGridSlot);}}if(resultGridSlots.Count==3){resultGridSlots.Add(sampleGridSlot);break;}resultGridSlots.Clear();}returnresultGridSlots.Count>0?newItemSequence<IUnityGridSlot>(GetType(),resultGridSlots):null;}
Finally, add theSquareShapeDetector
to the sequence detector list in theAppContext
class.
publicclassAppContext:MonoBehaviour,IAppContext{ ...privateISequenceDetector<IUnityGridSlot>[]GetSequenceDetectors(){returnnewISequenceDetector<IUnityGridSlot>[]{ ... newSquareShapeDetector()};} ...}
Let's create a stone item that is only destroyed when a match happens in one of the neighbour tiles.
Add aStone
value to theTileGroup
enum.
publicenumTileGroup{Unavailable=0,Available=1,Ice=2,Stone=3}
Create a classStoneState
and inherit from theStatefulGridTile
.
publicclassStoneState:StatefulGridTile{privatebool_isLocked=true;privatebool_canContainItem;privateint_group=(int)TileGroup.Stone;// Defines the tile group id.publicoverrideintGroupId=>_group;// Prevents the block from move.publicoverrideboolIsLocked=>_isLocked;// Prevents the item creation.publicoverrideboolCanContainItem=>_canContainItem;// Occurs when all block states have completed.protectedoverridevoidOnComplete(){_isLocked=false;_canContainItem=true;_group=(int)TileGroup.Available;}// Occurs when the block state is reset.protectedoverridevoidOnReset(){_isLocked=true;_canContainItem=false;_group=(int)TileGroup.Stone;}}
To respond to any changes in one of the neighbour tiles, we have to implement anISpecialItemDetector<TGridSlot>
interface. Create aStoneItemDetector
class and inherit from theISpecialItemDetector<TGridSlot>
.
publicclassStoneItemDetector:ISpecialItemDetector<IUnityGridSlot>{privatereadonlyGridPosition[]_lookupDirections;publicStoneItemDetector(){_lookupDirections=new[]{GridPosition.Up,GridPosition.Down,GridPosition.Left,GridPosition.Right};}publicIEnumerable<IUnityGridSlot>GetSpecialItemGridSlots(IGameBoard<IUnityGridSlot>gameBoard,IUnityGridSlotgridSlot){if(gridSlot.IsMovable==false){yieldbreak;}foreach(varlookupDirectionin_lookupDirections){varlookupPosition=gridSlot.GridPosition+lookupDirection;if(gameBoard.IsPositionOnGrid(lookupPosition)==false){continue;}varlookupGridSlot=gameBoard[lookupPosition];if(lookupGridSlot.State.GroupId==(int)TileGroup.Stone){yieldreturnlookupGridSlot;}}}}
Once theStoneItemDetector
is implemented. Register it in theAppContext
class.
publicclassAppContext:MonoBehaviour,IAppContext{ ...privateISpecialItemDetector<IUnityGridSlot>[]GetSpecialItemDetectors(){returnnewISpecialItemDetector<IUnityGridSlot>[]{ ... newStoneItemDetector()};} ...}
Next, move on to setting up the scene and prefabs.
First of all, add a block state sprites to theTilesSpriteAtlas
and create aStoneTilePrefab
prefab varian from theStatefulBlankPrefab
.
Configure theStoneTilePrefab
by adding theStoneState
script to it and filling in aState Sprite Names
list.
Note: You can create more than one visual state for a block by adding more state sprites.
Finally, select aGameBoard
object in the scene and add theStoneTilePrefab
to aGridTiles
list of theUnityGameBoardRenderer
script.
Video Demonstration
StoneBlockGameplay.mp4
You may contribute in several ways like creating new features, fixing bugs or improving documentation and examples.
Usediscussions to have conversations and post answers without opening issues.
Discussions is a place to:
- Share ideas
- Ask questions
- Engage with other community members
If you find a bug in the source code, pleasecreate bug report.
Please browseexisting 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, pleasesubmit 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 theMIT License.
About
SDK for creating Match 3 games