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<MoleController> ().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>().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>().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>().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>().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>().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>().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<Text>();
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);
}
}
}
}
/// <summary>
/// 加入频道
/// </summary>
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);
}
/// <summary>
/// 离开频道
/// </summary>
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();
}
}
/// <summary>
/// 查询 SDK 版本号。
/// </summary>
/// <returns></returns>
public string getSdkVersion()
{
string ver = IRtcEngine.GetSdkVersion();
return ver;
}
bool isMuted = false;
void MuteButtonTapped()
{
//设置静音或者取消静音
string labeltext = isMuted ? "静音" : "取消静音";
Text label = muteButton.GetComponentInChildren<Text>();
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
