文章目录
目录
前言
使用Unity制作一款消消乐,能让开发者了解Unity UI组件的使用,C#编写代码的能力得到提升。
一、游戏预制体
1.预制体介绍
预制体包含几种消除类型的枚举,枚举里面一个字段代表一种消除对象的种类。
public enum SweetsType
{
EMPTY,
NORMAL,
BARRIER,
ROW_CLEAR,
COLUMN_CLEAR,
RAINBOWCANDY,
WALL,
COUNT
}
简单介绍一下,有空类型、普通类型、障碍类型、行消除、列消除和同类型消除。
经常使用的是普通类型消除,这是我们用到的主要交互的游戏预制体。
2.普通类型预制体
在此预制体里面也有一个枚举类型的ColorType作为普通预制体的颜色区分,每种颜色对应一种图片。
public enum ColorType
{
YELLOW,
PURPLE,
RED,
BLUE,
GREEN,
PINK,
ANY,
COUNT
}
public struct ColorSprite
{
public ColorType color;
public Sprite sprite;
}
public ColorSprite[] ColorSprites;
预制体分类后,需要对预制体添加可移动,可按颜色区分,可消除的游戏逻辑。
//移动
public void Move(int newX, int newY, float time)
{
if (moveCoroutine != null)
{
StopCoroutine(moveCoroutine);
}
moveCoroutine = MoveCoroutine(newX, newY, time);
StartCoroutine(moveCoroutine);
}
//设置颜色类型
public void SetColor(ColorType newColor)
{
color = newColor;
if (colorSpriteDict.ContainsKey(newColor))
{
sprite.sprite = colorSpriteDict[newColor];
}
}
//消除
public virtual void Clear()
{
isClearing = true;
GameObject newSweet = Instantiate(gameObject,
sweet.gameManager.CorrectPosition(sweet.X, sweet.Y), Quaternion.identity);
newSweet.GetComponent<ClearSweet>().PlayClearAnimator();
Destroy(gameObject);
}
二、动画和音效
当游戏对象被拖拽至能匹配的位置,被删除时就要播放销毁动画和销毁音效。
使用Animation动画控制器,改变图片的Scale和透明度,以此达到销毁的效果。
当游戏对象匹配成功后,开启销毁函数,使用协程待销毁动画播放完毕之后才开始销毁物体。
在销毁之前,会播放销毁音效和增加玩家的得分。
private IEnumerator ClearCoroutine()
{
Animator animator = GetComponent<Animator>();
if(animator != null )
{
animator.Play(clearAnimation.name);
GameManager.Instance.PlayerScore++;
AudioSource.PlayClipAtPoint(DestoryAudio,transform.position);
yield return new WaitForSeconds(clearAnimation.length);
Destroy(gameObject);
}
}
三、主游戏界面
此部分开始涉及到ui 组件的使用,使用Text,Button,Image等基础组件拼接UI元素,使用Scroll Rect制作道具滑动列表。
Scroll Rect组件使用是要注意一下其结构,根物体挂载Scroll Rect组件,
其子物体viewport挂载一个Mask组件作为限制显示区域的遮罩,
content物体需挂载Grid Layout Group组件限制道具物体的间距以及排列方式。
四、消除玩法
1.确定行列
xColumn = lastLevelConfig.xColumn;
yRow = lastLevelConfig.yRow;
2.生成具体游戏对象
sweets = new GameSweet[xColumn, yRow];
for (int x = 0; x < xColumn; x++)
{
for (int y = 0; y < yRow; y++)
{
//生成墙体
if (IsExistXY(lastLevelConfig.wallPos, x, y))
{
CreateNewSweet(x, y, SweetsType.WALL);
continue;
}
CreateNewSweet(x, y, SweetsType.EMPTY);
}
}
//生成墙体
for (int i = 0; i < lastLevelConfig.wallPos.Length; i++)
{
int x = (int)lastLevelConfig.wallPos[i].x;
int y = (int)lastLevelConfig.wallPos[i].y;
Destroy(sweets[x, y].gameObject);
CreateNewSweet(x, y, SweetsType.WALL);
}
//生成具体对象
for (int i = 0; i < lastLevelConfig.sweetInfos.Length; i++)
{
SweetInfo sweetInfo = lastLevelConfig.sweetInfos[i];
ColorType color = sweetInfo.color;
SweetsType type = sweetInfo.type ;
int x = (int)sweetInfo.sweetPos.x;
int y = (int)sweetInfo.sweetPos.y;
DestoryAndCreateSweet(x, y, type, color);
}
//填充物品
public IEnumerator AllFill()
{
isMoving = false;
bool needRefill = true;
while (needRefill)
{
yield return new WaitForSeconds(fillTime);
while (Fill())
{
yield return new WaitForSeconds(fillTime);
}
needRefill = ClearAllMatchedSweet();
}
CheckGameWin();
isMoving = false;
}
//分布填充
public bool Fill()
{
bool filledNotFinished = false;//判断本次填充是否完成
for (int y = yRow - 2; y >= 0; y--)
{
for (int x = 0; x < xColumn; x++)
{
GameSweet sweet = sweets[x, y];//得到当前元素位置的甜品对象
if (sweet.CanMove())//如果无法移动,则无法往下填充
{
GameSweet sweetBelow = sweets[x, y + 1];
if (sweetBelow.Type == SweetsType.EMPTY) //垂直填充
{
Destroy(sweetBelow.gameObject);
sweet.MovedComponent.Move(x, y + 1, fillTime);
sweets[x, y + 1] = sweet;
CreateNewSweet(x, y, SweetsType.EMPTY);
filledNotFinished = true;
}
else //斜向填充
{
for (int down = -1; down <= 1; down++)
{
if (down != 0)
{
int downX = x + down;
if (downX >= 0 && downX < xColumn)
{
GameSweet downSweet = sweets[downX, y + 1];
if (downSweet.Type == SweetsType.EMPTY)
{
bool canfill = true;//用来判断垂直填充是否可以满足填充要求
for (int aboveY = y; aboveY >= 0; aboveY--)
{
GameSweet sweetAbove = sweets[downX, aboveY];
if (sweetAbove.CanMove())
{
break;
}
else if (!sweetAbove.CanMove() && sweetAbove.Type != SweetsType.EMPTY)
{
canfill = false;
break;
}
}
if (!canfill)
{
Destroy(downSweet.gameObject);
sweet.MovedComponent.Move(downX, y + 1, fillTime);
sweets[downX, y + 1] = sweet;
CreateNewSweet(x, y, SweetsType.EMPTY);
filledNotFinished = true;
break;
}
}
}
}
}
}
}
}
}
//最上排的特殊情况
for (int x = 0; x < xColumn; x++)
{
GameSweet sweet = sweets[x, 0];
if (sweet.Type == SweetsType.EMPTY)
{
GameObject newSweet = Instantiate(sweetPrefabDict[SweetsType.NORMAL], CorrectPosition(x, -1), Quaternion.identity);
newSweet.transform.parent = transform;
sweets[x, 0] = newSweet.GetComponent<GameSweet>();
sweets[x, 0].Init(x, -1, this, SweetsType.NORMAL);
sweets[x, 0].MovedComponent.Move(x, 0, fillTime);
sweets[x, 0].ColoredComponent.SetColor((ColorSweet.ColorType)Random.Range(0, sweets[x, 0].ColoredComponent.NumColors));
filledNotFinished = true;
}
}
return filledNotFinished;
}
3.使两个对象能进行交换
// 交换两个甜品的方法
private void ExchangeSweets(GameSweet sweet1, GameSweet sweet2)
{
if (sweet1.CanMove() && sweet2.CanMove())
{
sweets[sweet1.X, sweet1.Y] = sweet2;
sweets[sweet2.X, sweet2.Y] = sweet1;
if (MatchSweets(sweet1, sweet2.X, sweet2.Y) != null ||
MatchSweets(sweet2, sweet1.X, sweet1.Y) != null || sweet1.Type == SweetsType.RAINBOWCANDY || sweet2.Type == SweetsType.RAINBOWCANDY || itemUseStatus[4])
{
int tempX = sweet1.X;
int tempY = sweet1.Y;
sweet1.MovedComponent.Move(sweet2.X, sweet2.Y, fillTime);
sweet2.MovedComponent.Move(tempX, tempY, fillTime);
if (sweet1.Type == SweetsType.RAINBOWCANDY && sweet1.CanClear() && sweet2.CanClear())
{
ClearColorSweet clearColor = sweet1.GetComponent<ClearColorSweet>();
if (clearColor != null)
{
clearColor.ClearColor = sweet2.ColoredComponent.Color;
}
ClearSweet(sweet1.X, sweet1.Y);
}
if (sweet2.Type == SweetsType.RAINBOWCANDY && sweet1.CanClear() && sweet2.CanClear())
{
ClearColorSweet clearColor = sweet2.GetComponent<ClearColorSweet>();
if (clearColor != null)
{
clearColor.ClearColor = sweet1.ColoredComponent.Color;
}
ClearSweet(sweet2.X, sweet2.Y);
}
ClearAllMatchedSweet();
StartCoroutine(AllFill());
pressedSweet = null;
enteredSweet = null;
itemUseStatus[4] = false;
gameStep--;
}
else
{
sweets[sweet1.X, sweet1.Y] = sweet1;
sweets[sweet2.X, sweet2.Y] = sweet2;
}
}
}
4.满足条件进行消除
i.先判断对象是否满足匹配方式(三个以上的相同类型对象同行或者同列)
//匹配方法
public List<GameSweet> MatchSweets(GameSweet sweet, int newX, int newY)
{
if (sweet.CanColor())
{
ColorSweet.ColorType color = sweet.ColoredComponent.Color;
List<GameSweet> matchRowSweets = new List<GameSweet>();
List<GameSweet> matchLineSweets = new List<GameSweet>();
List<GameSweet> finishedMathchingSweets = new List<GameSweet>();
//行匹配
matchRowSweets.Add(sweet);
//i = 0代表:当前交换目标的左边,i=1代表往右
for (int i = 0; i <= 1; i++)
{
for (int xDistance = 1; xDistance < xColumn; xDistance++)
{
int x;//实际要判断的目标
if (i == 0)
{
x = newX - xDistance;//向左边遍历
}
else
{
x = newX + xDistance;//向右遍历
}
if (x < 0 || x >= xColumn)//边界
{
break;
}
if (sweets[x, newY].CanColor() && sweets[x, newY].ColoredComponent.Color == color)//相邻的颜色与基准颜色相等
{
matchRowSweets.Add(sweets[x, newY]);//将符合条件的甜品放入匹配队列
}
else
{
break;
}
}
}
if (matchRowSweets.Count >= 3)
{
for (int i = 0; i < matchRowSweets.Count; i++)
{
finishedMathchingSweets.Add(matchRowSweets[i]);
}
}
//L T型匹配
//检查当前行遍历列表中的元素是否大于3
if (matchRowSweets.Count >= 3)
{
for (int i = 0; i < matchRowSweets.Count; i++)
{
//循环将满足条件的甜品放入
//finishedMathchingSweets.Add(matchRowSweets[i]);
//在行匹配列表满足匹配的情况下,每个元素进行列遍历
//0代表上方,1代表下方
for (int j = 0; j <= 1; j++)
{
//根据基准位置依次往上遍历的距离
for (int yDistance = 1; yDistance < yRow; yDistance++)
{
int y;
if (j == 0)
{
y = newY - yDistance;
}
else
{
y = newY + yDistance;
}
if (y < 0 || y >= yRow)
{
break;
}
if (sweets[matchRowSweets[i].X, y].CanColor() && sweets[matchRowSweets[i].X, y].ColoredComponent.Color == color)
{
matchLineSweets.Add(sweets[matchRowSweets[i].X, y]);
}
else
{
break;
}
}
}
if (matchLineSweets.Count < 2)
{
matchLineSweets.Clear();
}
else
{
for (int j = 0; j < matchLineSweets.Count; j++)
{
finishedMathchingSweets.Add(matchLineSweets[j]);
}
break;
}
}
}
if (finishedMathchingSweets.Count >= 3)
{
return finishedMathchingSweets;
}
matchRowSweets.Clear();
matchLineSweets.Clear();
//列匹配
matchLineSweets.Add(sweet);
//i = 0代表:当前交换目标的左边,i=1代表往右
for (int i = 0; i <= 1; i++)
{
for (int yDistance = 1; yDistance < yRow; yDistance++)
{
int y;//实际要判断的目标
if (i == 0)
{
y = newY - yDistance;//向左边遍历
}
else
{
y = newY + yDistance;//向右遍历
}
if (y < 0 || y >= yRow)//边界
{
break;
}
if (sweets[newX, y].CanColor() && sweets[newX, y].ColoredComponent.Color == color)//相邻的颜色与基准颜色相等
{
matchLineSweets.Add(sweets[newX, y]);//将符合条件的甜品放入匹配队列
}
else
{
break;
}
}
}
if (matchLineSweets.Count >= 3)
{
for (int i = 0; i < matchLineSweets.Count; i++)
{
finishedMathchingSweets.Add(matchLineSweets[i]);//不一样
}
}
//
//L T型匹配
//检查当前行遍历列表中的元素是否大于3
if (matchLineSweets.Count >= 3)
{
for (int i = 0; i < matchLineSweets.Count; i++)
{
//循环将满足条件的甜品放入
//finishedMathchingSweets.Add(matchRowSweets[i]);
//在行匹配列表满足匹配的情况下,每个元素进行列遍历
//0代表上方,1代表下方
for (int j = 0; j <= 1; j++)
{
//根据基准位置依次往上遍历的距离
for (int xDistance = 1; xDistance < xColumn; xDistance++)
{
int x;
if (j == 0)
{
x = newY - xDistance;
}
else
{
x = newY + xDistance;
}
if (x < 0 || x >= xColumn)
{
break;
}
if (sweets[x, matchLineSweets[i].Y].CanColor() && sweets[x, matchLineSweets[i].Y].ColoredComponent.Color == color)
{
matchRowSweets.Add(sweets[x, matchLineSweets[i].Y]);
}
else
{
break;
}
}
}
if (matchRowSweets.Count < 2)
{
matchRowSweets.Clear();
}
else
{
for (int j = 0; j < matchRowSweets.Count; j++)
{
finishedMathchingSweets.Add(matchRowSweets[j]);
}
break;
}
}
}
if (finishedMathchingSweets.Count >= 3)
{
return finishedMathchingSweets;
}
}
//全部都不满足
return null;
}
ii.对于满足条件的游戏对象存入匹配数组中,并且进行消除
//清除方法
public bool ClearSweet(int x, int y)
{
if (sweets[x, y].CanClear() && !sweets[x, y].ClearedComponent.IsClearing)
{
hasCleanSweetType[sweets[x, y].Type]++;
if(sweets[x, y].CanColor())
hasCleanSweetColor[sweets[x, y].ColoredComponent.Color]++;
sweets[x, y].ClearedComponent.Clear();//调用清除协程
CreateNewSweet(x, y, SweetsType.EMPTY);
ClearBarrier(x, y);
return true;
}
return false;
}
//消除匹配条件的对象
private bool ClearAllMatchedSweet()
{
bool neewRefill = false;
for (int y = 0; y < yRow; y++)
{
for (int x = 0; x < xColumn; x++)
{
if (sweets[x, y].CanClear())
{
List<GameSweet> matchList = MatchSweets(sweets[x, y], x, y);
if (matchList != null)
{
SweetsType specialSweetsType = SweetsType.COUNT;//是否需要产生特殊对象
//随机对象的位置
GameSweet randomSweet = matchList[Random.Range(0, matchList.Count)];
int specialSweetX = randomSweet.X;
int specialSweetY = randomSweet.Y;
//消除的对象数量为4才生成对象
if (matchList.Count == 4)
{
//int Random.Range(int min,int max)
//这个得到的是int类型的随机数,需要注意的是,随机数的取值范围包括min,但不包括max;
specialSweetsType = (SweetsType)Random.Range((int)SweetsType.ROW_CLEAR, (int)SweetsType.COLUMN_CLEAR + 1);
}
//5个就产生消除同类型的对象
else if (matchList.Count >= 5)
{
specialSweetsType = SweetsType.RAINBOWCANDY;
}
for (int i = 0; i < matchList.Count; i++)
{
if (ClearSweet(matchList[i].X, matchList[i].Y))
{
neewRefill = true;
}
}
if (specialSweetsType != SweetsType.COUNT)
{
Destroy(sweets[specialSweetX, specialSweetY].gameObject);
GameSweet newSweet = CreateNewSweet(specialSweetX, specialSweetY, specialSweetsType);
if (specialSweetsType == SweetsType.ROW_CLEAR || specialSweetsType == SweetsType.COLUMN_CLEAR && newSweet.CanColor() && matchList[0].CanColor())
{
newSweet.ColoredComponent.SetColor(matchList[0].ColoredComponent.Color);
}
//特殊类型的产生
else if (specialSweetsType == SweetsType.RAINBOWCANDY && newSweet.CanColor())
{
newSweet.ColoredComponent.SetColor(ColorSweet.ColorType.ANY);
}
}
}
}
}
}
return neewRefill;
}
五、道具制作
消消乐游戏中一些常见的道具,例如“增加步数”、“增加倒计时”、“改变物体类型”、“强制交换”、“炸弹消除部分区域”、“刷新所有物体”等。此部分的道具都是使用了,Button组件来绑定对应的函数事件来触发道具效果。
//加5步
public void AddFiveExtraMoves()
{
if (playerData.items[0] <= 0)
{
if (countdownCoroutine != null)
{
StopCoroutine(countdownCoroutine);
}
countdownCoroutine = StartCoroutine(CountdownCoroutine());
return;
}
Array.Clear(itemUseStatus, 0, itemUseStatus.Length);
gameStep += 5;
//点击使用道具,发送扣除道具
socketConnector.SendUseItem(1);
Debug.Log("扣除道具1");
}
//石化猫咪
public void MakeCatStoned()
{
if (playerData.items[1] <= 0)
{
if (countdownCoroutine != null)
{
StopCoroutine(countdownCoroutine);
}
countdownCoroutine = StartCoroutine(CountdownCoroutine());
return;
}
Array.Clear(itemUseStatus, 0, itemUseStatus.Length);
itemUseStatus[5] = true;
socketConnector.SendUseItem(2);
Debug.Log("扣除道具2");
}
//范围炸弹
public void BombInRange()
{
if (playerData.items[4] <= 0)
{
if (countdownCoroutine != null)
{
StopCoroutine(countdownCoroutine);
}
countdownCoroutine = StartCoroutine(CountdownCoroutine());
return;
}
Array.Clear(itemUseStatus, 0, itemUseStatus.Length);
itemUseStatus[6] = true;
socketConnector.SendUseItem(5);
Debug.Log("扣除道具5");
}
六、登录、商店、选关等界面
1.登录界面
ui元素加上动画控制器,控制图片旋转,位移,缩放,不透明等变化。输入账号密码即可登录登录。
2.选关界面
选关列表使用到的UI组件整体上与前文提及使用的方法相似,此处的关卡数量是依据本地的json数据,根据关卡数量来生成按钮数量。此时的金币数量和道具数量是依据服务器的socket发送回来的数据读取并显示。
3.商店界面
商店界面UI使用的组件也与前文提及的类似,客户端根据服务器发送的道具数量,来生成具体的物品数量,点击购买就会发送对应的物品id到服务器,待服务器确认数据后,发送消息回来客户端,客户端刷新道具数量。
七、连接服务器实现登录购买等功能
1.使用WebSocket插件连接服务器
具体参考GitHub - psygames/UnityWebSocket: :whale: The Best Unity WebSocket Plugin for All Platforms.
// 命名空间
using UnityWebSocket;
// 创建实例
string address = "ws://echo.websocket.org";
WebSocket socket = new WebSocket(address);
// 注册回调
socket.OnOpen += OnOpen;
socket.OnClose += OnClose;
socket.OnMessage += OnMessage;
socket.OnError += OnError;
// 连接
socket.ConnectAsync();
// 发送 string 类型数据
socket.SendAsync(str);
// 或者 发送 byte[] 类型数据(建议使用)
socket.SendAsync(bytes);
// 关闭连接
socket.CloseAsync();
2.实现登录功能
发送登录信息
public void LoginSend(string username,string password)
{
Socket_Send("login|" + username + "|"+password);
}
接收服务器返回的信息
private void Socket_OnMessage(object sender, MessageEventArgs e)
{
Debug.Log(string.Format("Receive: {0}", e.Data));
gameManager = FindObjectOfType<GameManager>();
selectLevel = FindObjectOfType<SelectLevel>();
string[] args = e.Data.Split('|');//则根据‘|’分割成字符串数组
switch (args[0])
{
case "login":
if (args[1] == "1")
{
//登录成功的代码
SceneManager.LoadScene(2);
Debug.Log("login successful");
}
break;
}
}
连接成功后服务器会校验账号密码是否正确,正确则返回确认信息并跳转到第二个关卡,注意关卡跳转期间要保WebSocket脚本不要被销毁,使用DontDestroyOnLoad()函数将脚本保护起来
3.实现商店购买功能
商店的物品使用金币来购买,客户端需要发送增加金币的函数
public void SendAddGold(int amount)
{
Socket_Send("addgold|" + amount);
}
金币数量满足购买条件后,发送购买协议到服务器
public void AddItemSend(string[] str)
{
string tempstr = "additem";
for (int i = 0; i < str.Length; i++)
{
tempstr += ( "|" + str[i]);
}
Socket_Send(tempstr);
}
购买成功后,客户端接收数据,并刷新道具数量和金币数量
case "item":
for(int i = 0; i < playerData.items.Length;i++)
{
if (i + 1 >= args.Length) break;
//可能有异常,留着以后增强代码健壮性
playerData.items[i] = int.Parse(args[i + 1]);
}
if (gameManager != null && gameManager.IsGaming)
{
Debug.Log("刷新道具数据");
gameManager.SetItems();
}
if (selectLevel != null && selectLevel.IsRunning)
{
Debug.Log("刷新道具数据");
selectLevel.SetItems();
}
break;
case "gold":
playerData.gold = int.Parse(args[1]);
if (selectLevel != null && selectLevel.IsRunning)
{
Debug.Log("刷新金币");
selectLevel.SetGold();
}
break;
八、使用json配置游戏关卡
使游戏数据能够存入json
public void SaveConfig(string filePath)
{
string json = JsonUtility.ToJson(gameConfig, true);
System.IO.File.WriteAllText(filePath, json);
}
// 保存配置到文件
string configFilePath = Path.Combine(Application.streamingAssetsPath, "config.json");
SaveConfig(configFilePath);
读取json,这里有两种方法,一种是System.IO.File.ReadAllText(filePath),另一种是UnityWebRequest.Get(filePath))。使用第一种ReadAllText方法在unity的编辑器模式下运行能正常读取到json文件,当资源被打包成APK后就不能读取到json文件,使用第二种UnityWebRequest.Get(filePath))方法,需要将json文件放置在StreamingAssets文件夹下,在此文件夹的资源不会被压缩,能正常读取到。
public void LoadConfig(string filePath)
{
StartCoroutine(LoadFromStreamingAssets(filePath, (json) =>
{
gameConfig = JsonUtility.FromJson<GameConfig>(json);
Debug.Log("配置加载成功");
}, (error) =>
{
Debug.LogError("配置加载失败:" + error);
}));
}
public IEnumerator LoadFromStreamingAssets(string filePath, Action<string> successCallback, Action<string> failCallback)
{
using (UnityWebRequest www = UnityWebRequest.Get(filePath))
{
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
failCallback?.Invoke(www.error);
}
else
{
successCallback?.Invoke(www.downloadHandler.text);
}
}
}
//public void LoadConfig(string filePath)
//{
// if (System.IO.File.Exists(filePath))
// {
// string json = System.IO.File.ReadAllText(filePath);
// gameConfig = JsonUtility.FromJson<GameConfig>(json);
// Debug.Log("配置加载成功");
// }
// else
// {
// Debug.LogError("配置文件不存在");
// }
//}
json部分关卡数据
{
"xColumn": 5,
"yRow": 5,
"cookiePos": [],
"wallPos": [],
"cookieBlood": [],
"levelGoalBySweetType": [
{
"x": 1.0,
"y": 70.0
},
{
"x": 2.0,
"y": 3.0
}
],
"levelGoalByColor": [],
"sweetInfos": [
{
"type": 3,
"color": 0,
"sweetPos": {
"x": 1.0,
"y": 2.0
}
},
{
"type": 4,
"color": 0,
"sweetPos": {
"x": 1.0,
"y": 3.0
}
}
]
},
九、广告SDK接入
这里选着接入TapTap的SDK,详情可以查看TapTap的开发者文档。
TapADN SDK 接入指南 | TapTap 开发者文档
1.SDK配置
- TapADN SDK 从 3.16.3.10 版本开始更新了 glide 的依赖,glide 版本从 4.0.0 更新到了 4.9.0。
- 3.16.3.17 加入 Android Dependencies 文件(位于 TapAD/Editor/TapAdDependencies.xml) 方便接入 EDM4U(https://github.com/googlesamples/unity-jar-resolver) 的项目方解决 Android 依赖。
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// 加入的依赖库-开始
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.0.1'
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
implementation "com.android.support:appcompat-v7:28.0.0"
implementation "com.android.support:support-annotations:28.0.0"
implementation "com.android.support:support-v4:28.0.0"
implementation "com.github.bumptech.glide:glide:4.9.0"
implementation 'com.android.support:recyclerview-v7:28.0.0'
// 加入的依赖库-结束
// 下面这行是 Unity 的 mainTemplate.gradle 自带的,帮助定位插入位置
// **DEPS**
}
2.权限申请
无论 Unity 版本都需加入 Android 相关权限申请,在 Project Settings
-> Player
-> Android Tab
-> Publish Settings
-> Build
,勾选Custom Main Manifest
。
将以下更改应用于生成的这个文件: Assets/Plugins/Android/AndroidManifest.xml
如果存在,请移除文件顶部的以下注释:
// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
修改文件内容:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.unity3d.player"
xmlns:tools="http://schemas.android.com/tools">
<!-- TapAd 必须的权限-开始 -->
<!-- TargetVersion 31 及以上 通过时,需要该权限) deviceName 和下面的 BLUETOOTEH 互斥-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- 广告获取坐标(经度、纬度、精度半径(米)、获取时间 毫秒)精准推送 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- IMEI 、序列号、MEID 、IMSI 、 ICCID 等信息。TargetSdkVersion 4 以及更高需要申请 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!-- TapAd 必须的权限-结束 -->
<!-- TapAd 可选择权限-开始 -->
<!-- 获取网络状态信息 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!-- 获取安装应用列表 Android 11 及以上版本才需声明,Android 11 以下版本无需申请 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<!-- (targetVersion 31 以下)deviceName 和上面的 BLUETOOTH_CONNECT 互斥-->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- 允许应用请求安装软件包 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<!-- TapAd 可选择权限-结束 -->
...
3.获取广告权限
using TapTap.TapAd
TapAdSdk.RequestPermissionIfNecessary();
4.初始化
using TapTap.TapAd
TapAdConfig config = new TapAdConfig.Builder()
.MediaId(your_media_id) // 必选参数,为 TapADN 注册的媒体 ID
.MediaName(your_media_name) // 必选参数,为 TapADN 注册的媒体名称
.MediaKey(your_media_key) // 必选参数,媒体密钥,可以在 TapADN 后台查看(用于传输数据的解密)
.MediaVersion("1") // 必选参数,默认值 "1"
.Channel(your__channel) // 必选参数,渠道
.TapClientId(your_tap_client_id) // 可选参数,TapTap 开发者中心的游戏 Client ID
.EnableDebugLog(false) // 可选参数,是否打开原生 debug 调试信息输出:true 打开、false 关闭。默认 false 关闭
.Build();
// CustomControllerWrapper 为实现了 TapTap.TapAd.ICustomController 的类
// onInitedCallback 为可选回调函数,类型是 System.Action,
TapAdSdk.Init(config, new CustomControllerWrapper(this), onInitedCallback);
5.获取广告
using TapTap.TapAd
TapSplashAd _tapSplashAd = null;
if (TapAdSdk.IsInited == false)
{
Debug.Log("TapAd 需要先初始化!");
return;
}
// 释放之前的广告
if (_tapSplashAd != null)
{
_tapSplashAd.Dispose();
_tapSplashAd = null;
}
int adId = YOUR_AD_ID;
// create AdRequest
var request = new TapAdRequest.Builder()
.SpaceId(adId)
.Build();
_tapSplashAd = new TapSplashAd(request);
// SplashAdLoadListener 为实现了 ISplashAdLoadListener 的类
_tapSplashAd.SetLoadListener(new SplashAdLoadListener(this));
_tapSplashAd.Load();
// Splash 加载接口说明
public interface ISplashAdLoadListener : ICommonLoadListener
{
// 当 Splash 加载完毕
void OnSplashAdLoad(TapSplashAd ad);
}
// 通用加载接口说明
public interface ICommonLoadListener
{
// 加载出错回调
void OnError(int code, string message);
}
6.播放广告
using TapTap.TapAd
if (TapAdSdk.IsInited == false)
{
Debug.Log("TapAd 需要先初始化!");
return;
}
if (_tapSplashAd != null)
{
// SplashInteractionListener 为实现了 ISplashAdInteractionListener 的类
_tapSplashAd.SetInteractionListener(new SplashInteractionListener(this));
_tapSplashAd.Show();
}
else
{
Debug.LogErrorFormat($"[Unity::AD] 未加载好视频,无法播放!");
}
// Splash 播放回调接口说明
public interface ISplashAdInteractionListener : ICommonInteractionListener
{
// 点击跳过
void OnAdSkip(TapSplashAd ad);
// 广告时间到
void OnAdTimeOver(TapSplashAd ad);
}
// 通用播放回调接口说明
public interface ICommonInteractionListener
{
}
广告播放完毕可以发放玩家奖励
public void OnAdClose(TapRewardVideoAd ad)
{
example.ShowText("[Unity::AD] {ad.AdType} OnAdClose/n发送了13金币");
Debug.Log("测试sdk结束输出,发送了13金币");
example.socketConnector.SendAddGold(13);
}
广告样例:
十、打包安卓APK
打开Build Setttings设置,平台选中Android,将需要打包的场景勾选上。
打开Player Settings,在Publishing Settings中点击Keystore Manager生成一个keystore。
设置好后选中你生成的keystore,并且输入密码。
接着配置好打包环境,选着Edit -- Preferences -- External Tools ,配置JDK,SDK,NDK等路径,勾选上方框使用官方推荐的即可。
以上设置完成后即可在Build Settings里面点击Build生成安卓APK安装包。
总结
以上就是简单的介绍了unity制作消消乐的流程,和一些游戏系统里面该有的模块制作。