Creating a Whack‑a‑Mole Mini‑Game in Unity with Agora Voice SDK for Global Chat
This tutorial walks through building a simple Whack‑a‑Mole Unity game, adding scene objects and state‑machine scripts, then integrating Agora's voice SDK to create a worldwide voice chat channel, complete with code examples and step‑by‑step instructions.
Preface
Two days ago I started exploring Agora's voice call SDK and thought it would be fun to build a simple Whack‑a‑Mole mini‑game and integrate a global voice chat channel.
Whack‑a‑Mole Mini‑Game
This game can be created with three scripts and is quite entertaining.
Below is a screenshot of the gameplay and the following steps describe how to build it.
Step 1: Build Scene and Model Configuration
Create a basic scene, add several pits where the moles will appear, place a hammer that will strike when the mouse is clicked, and hide a cat model under a pit.
Step 2: Write the Cat State Machine Script
Declare the possible states (UNDER_GROUND, UP, ON_GROUND, DOWN, HIT) and implement the Update method that moves the cat according to its current state.
enum State{
UNDER_GROUND,
UP,
ON_GROUND,
DOWN,
HIT,
}
State state;
void Update ()
{
if (this.state == State.UP)
{
transform.Translate (0, this.moveSpeed, 0);
if (transform.position.y > TOP)
{
transform.position =
new Vector3 (transform.position.x, TOP, transform.position.z);
this.state = State.ON_GROUND;
this.tmpTime = 0;
}
}
else if (this.state == State.ON_GROUND)
{
this.tmpTime += Time.deltaTime;
if (this.tmpTime > this.waitTime)
{
this.state = State.DOWN;
}
}
else if (this.state == State.DOWN)
{
transform.Translate (0, -this.moveSpeed, 0);
if (transform.position.y < BOTTOM)
{
transform.position =
new Vector3(transform.position.x, BOTTOM, transform.position.z);
this.state = State.UNDER_GROUND;
}
}
}The cat now displays different behaviours based on its state.
Step 3: Mouse‑Click Hammer Script
Detect mouse clicks, cast a ray to find the mole, trigger a hit effect, and increase the score.
void Update ()
{
if(Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, 100))
{
GameObject mole = hit.collider.gameObject;
bool isHit = mole.GetComponent
().Hit ();
// if hit the mole, show hummer and effect
if (isHit)
{
StartCoroutine (Hit (mole.transform.position));
ScoreManager.score += 10;
}
}
}
}Step 4: Random Cat Spawn Script
Use a coroutine to generate moles at random intervals, limiting the number of simultaneous moles.
IEnumerator Generate()
{
this.generate = true;
while (this.generate)
{
// wait to generate next group
yield return new WaitForSeconds (1.0f);
int n = moles.Count;
int maxNum = (int) this.maxMoles.Evaluate ( GameManager.time );
for (int i = 0; i < maxNum; i++)
{
// select mole to up
this.moles [Random.Range (0, n)].Up ();
yield return new WaitForSeconds (0.3f);
}
}
}Step 5: Download Agora Audio SDK and Create a Project
Visit the Agora website, register, download the Unity audio SDK, and create a project in the console to obtain an App ID.
Images showing the download page and console configuration are included.
Step 6: Integrate Agora Audio SDK into Unity
Import the SDK into the Unity project, add a script to manage the world‑chat channel, and configure callbacks for joining, leaving, user events, volume indication, warnings, errors, and statistics.
using UnityEngine;
using UnityEngine.UI;
#if (UNITY_2018_3_OR_NEWER)
using UnityEngine.Android;
#endif
using agora_gaming_rtc;
public class HelloUnity3D : MonoBehaviour
{
private InputField mChannelNameInputField; //频道号
public Text mShownMessage; //提示
private Text versionText; //版本号
public Button joinChannel; //加入房间
public Button leaveChannel; //离开房间
private Button muteButton; //静音
private IRtcEngine mRtcEngine = null;
// 输入App ID后,在App ID外删除##
[SerializeField]
private string AppID = "app_id";
void Awake()
{
QualitySettings.vSyncCount = 0;
Application.targetFrameRate = 30;
//muteButton.enabled = false;
CheckAppId();
}
// 进行初始化
void Start()
{
#if (UNITY_2018_3_OR_NEWER)
// 判断是否有麦克风权限,没有权限的话主动申请权限
if (!Permission.HasUserAuthorizedPermission(Permission.Microphone))
{
Permission.RequestUserPermission(Permission.Microphone);
}
#endif
joinChannel.onClick.AddListener(JoinChannel);
leaveChannel.onClick.AddListener(LeaveChannel);
//muteButton.onClick.AddListener(MuteButtonTapped);
mRtcEngine = IRtcEngine.GetEngine(AppID);
//versionText.GetComponent
().text = "Version : " + getSdkVersion();
//加入频道成功后的回调
mRtcEngine.OnJoinChannelSuccess += (string channelName, uint uid, int elapsed) =>
{
string joinSuccessMessage = string.Format("加入频道 回调 uid: {0}, channel: {1}, version: {2}", uid, channelName, getSdkVersion());
Debug.Log(joinSuccessMessage);
mShownMessage.GetComponent
().text = (joinSuccessMessage);
//muteButton.enabled = true;
};
//离开频道回调。
mRtcEngine.OnLeaveChannel += (RtcStats stats) =>
{
string leaveChannelMessage = string.Format("离开频道回调时间 {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}", stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate);
Debug.Log(leaveChannelMessage);
mShownMessage.GetComponent
().text = (leaveChannelMessage);
//muteButton.enabled = false;
// 重置静音键状态
//if (isMuted)
// {
// MuteButtonTapped();
// }
};
//远端用户加入当前频道回调。
mRtcEngine.OnUserJoined += (uint uid, int elapsed) =>
{
string userJoinedMessage = string.Format("远端用户加入当前频道回调 uid {0} {1}", uid, elapsed);
Debug.Log(userJoinedMessage);
mShownMessage.GetComponent
().text = (userJoinedMessage);
};
//远端用户离开当前频道回调。
mRtcEngine.OnUserOffline += (uint uid, USER_OFFLINE_REASON reason) =>
{
string userOfflineMessage = string.Format("远端用户离开当前频道回调 uid {0} {1}", uid, reason);
Debug.Log(userOfflineMessage);
mShownMessage.GetComponent
().text = (userOfflineMessage);
};
// 用户音量提示回调。
mRtcEngine.OnVolumeIndication += (AudioVolumeInfo[] speakers, int speakerNumber, int totalVolume) =>
{
if (speakerNumber == 0 || speakers == null)
{
Debug.Log(string.Format("本地用户音量提示回调 {0}", totalVolume));
}
for (int idx = 0; idx < speakerNumber; idx++)
{
string volumeIndicationMessage = string.Format("{0} onVolumeIndication {1} {2}", speakerNumber, speakers[idx].uid, speakers[idx].volume);
Debug.Log(volumeIndicationMessage);
}
};
//用户静音提示回调
mRtcEngine.OnUserMutedAudio += (uint uid, bool muted) =>
{
string userMutedMessage = string.Format("用户静音提示回调 uid {0} {1}", uid, muted);
Debug.Log(userMutedMessage);
mShownMessage.GetComponent
().text = (userMutedMessage);
};
//发生警告回调
mRtcEngine.OnWarning += (int warn, string msg) =>
{
string description = IRtcEngine.GetErrorDescription(warn);
string warningMessage = string.Format("发生警告回调 {0} {1} {2}", warn, msg, description);
Debug.Log(warningMessage);
};
//发生错误回调
mRtcEngine.OnError += (int error, string msg) =>
{
string description = IRtcEngine.GetErrorDescription(error);
string errorMessage = string.Format("发生错误回调 {0} {1} {2}", error, msg, description);
Debug.Log(errorMessage);
};
// 当前通话统计回调,每两秒触发一次。
mRtcEngine.OnRtcStats += (RtcStats stats) =>
{
string rtcStatsMessage = string.Format("onRtcStats callback duration {0}, tx: {1}, rx: {2}, tx kbps: {3}, rx kbps: {4}, tx(a) kbps: {5}, rx(a) kbps: {6} users {7}",
stats.duration, stats.txBytes, stats.rxBytes, stats.txKBitRate, stats.rxKBitRate, stats.txAudioKBitRate, stats.rxAudioKBitRate, stats.userCount);
//Debug.Log(rtcStatsMessage);
int lengthOfMixingFile = mRtcEngine.GetAudioMixingDuration();
int currentTs = mRtcEngine.GetAudioMixingCurrentPosition();
string mixingMessage = string.Format("Mixing File Meta {0}, {1}", lengthOfMixingFile, currentTs);
//Debug.Log(mixingMessage);
};
//语音路由已发生变化回调。(只在移动平台生效)
mRtcEngine.OnAudioRouteChanged += (AUDIO_ROUTE route) =>
{
string routeMessage = string.Format("onAudioRouteChanged {0}", route);
Debug.Log(routeMessage);
};
//Token 过期回调
mRtcEngine.OnRequestToken += () =>
{
string requestKeyMessage = string.Format("OnRequestToken");
Debug.Log(requestKeyMessage);
};
//网络中断回调(建立成功后才会触发)
mRtcEngine.OnConnectionInterrupted += () =>
{
string interruptedMessage = string.Format("OnConnectionInterrupted");
Debug.Log(interruptedMessage);
};
//网络连接丢失回调
mRtcEngine.OnConnectionLost += () =>
{
string lostMessage = string.Format("OnConnectionLost");
Debug.Log(lostMessage);
};
// 设置 Log 级别
mRtcEngine.SetLogFilter(LOG_FILTER.INFO);
//1.设置为自由说话模式,常用于一对一或者群聊
mRtcEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_COMMUNICATION);
//2.设置为直播模式,适用于聊天室或交互式视频流等场景。
//mRtcEngine.SetChannelProfile (CHANNEL_PROFILE.CHANNEL_PROFILE_LIVE_BROADCASTING);
//3.设置为游戏模式。这个配置文件使用较低比特率的编解码器,消耗更少的电力。适用于所有游戏玩家都可以自由交谈的游戏场景。
//mRtcEngine.SetChannelProfile(CHANNEL_PROFILE.CHANNEL_PROFILE_GAME);
//设置直播场景下的用户角色。
//mRtcEngine.SetClientRole (CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);
}
private void CheckAppId()
{
Debug.Assert(AppID.Length > 10, "请先在Game Controller对象上填写你的AppId。.");
GameObject go = GameObject.Find("AppIDText");
if (go != null)
{
Text appIDText = go.GetComponent
();
if (appIDText != null)
{
if (string.IsNullOrEmpty(AppID))
{
appIDText.text = "AppID: " + "UNDEFINED!";
appIDText.color = Color.red;
}
else
{
appIDText.text = "AppID: " + AppID.Substring(0, 4) + "********" + AppID.Substring(AppID.Length - 4, 4);
}
}
}
}
///
/// 加入频道
///
public void JoinChannel()
{
// 从界面的输入框获取频道名称
string channelName = "adc666";
// string channelNameOld = mChannelNameInputField.text.Trim();
Debug.Log(string.Format("从界面的输入框获取频道名称 {0}", channelName));
if (string.IsNullOrEmpty(channelName))
{
return;
}
// 加入频道
// channelKey: 动态秘钥,我们最开始没有选择 Token 模式,这里就可以传入 null;否则需要传入服务器生成的 Token
// channelName: 频道名称
// info: 开发者附带信息(非必要),不会传递给频道内其他用户
// uid: 用户ID,0 为自动分配
mRtcEngine.JoinChannelByKey(channelKey: null, channelName: channelName, info: "extra", uid: 0);
//加入频道并设置发布和订阅状态。
//mRtcEngine.JoinChannel(channelName, "extra", 0);
}
///
/// 离开频道
///
public void LeaveChannel()
{
// 离开频道
mRtcEngine.LeaveChannel();
string channelName = "abc666";
Debug.Log(string.Format("left channel name {0}", channelName));
}
void OnApplicationQuit()
{
if (mRtcEngine != null)
{
// 销毁 IRtcEngine
IRtcEngine.Destroy();
}
}
///
/// 查询 SDK 版本号。
///
///
public string getSdkVersion()
{
string ver = IRtcEngine.GetSdkVersion();
return ver;
}
bool isMuted = false;
void MuteButtonTapped()
{
//设置静音或者取消静音
string labeltext = isMuted ? "静音" : "取消静音";
Text label = muteButton.GetComponentInChildren
();
if (label != null)
{
label.text = labeltext;
}
isMuted = !isMuted;
// 设置静音(停止推送本地音频)
mRtcEngine.EnableLocalAudio(!isMuted);
Debug.Log("静音方法执行完成");
}
}The script also handles microphone permission, mute/unmute toggling, and channel profile configuration.
Conclusion
The article demonstrates a simple Whack‑a‑Mole game with Agora voice SDK integration.
It is a personal side project that still has many areas for improvement.
The tutorial is concise and enjoyable.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.