Game Development 17 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Creating a Whack‑a‑Mole Mini‑Game in Unity with Agora Voice SDK for Global Chat

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Game Developmentc++UnityAgoravoice chatWhack-a-Mole
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.