[OPEN SOURCE] AchievementService | Easily Manage and Award Badges | v1.03

AchievementService

AchievementService

Easily Manage and Award Badges

Module (Toolbox) | Game Example (Uncopylocked)

Module (.rbxm)

AchievementService is an open-source module designed to make managing and awarding achievements in your games easy and customizable. With built-in support for UI animations, haptics, audio, and more, you can create engaging achievement experiences for your players.


⭐ Features

1. Awarding Badges

AchievementService makes it simple to award badges to players in your game.
AchievementService:Award(player, badgeIdentifier) -- badgeIdentifier can either be the BadgeName or BadgeId

That’s it! You can now use either the BadgeName or the BadgeId to award a badge, making it even more flexible. AchievementService will use HTTPService and the Roblox API to automatically fetch and manage the available badges for your game.

2. Customizable Animations

AchievementService provides an option to toggle customizable UI animations. It also includes a default animation for easy use. You can either use the default UI or design your own. I recommend using the existing script in the default UI as a template if you decide to create your own.

3. Audio Support

AchievementService allows for customizable sounds, allowing you to toggle and add audios. I've also included a pack of 25 classic Roblox audios with the module. lol

4. Haptic Feedback

AchievementService even allows for haptics, allowing for tactile response to mobile and possibly console players.

5. Fully Open-Source

AchievementService is entirely open-source, allowing you to modify and extend the module as needed for your game.

❓ How It Works

When you initialize the module using AchievementService.init(), it handles badge management based on your chosen configuration:

  1. Using HTTPService (Default):
    By default, AchievementService fetches all the badges in your game using the Roblox API and compiles them into an internal dictionary. This initialization process only happens once, ensuring that subsequent calls to :Award() are fast and efficient. Instead of making additional API calls, the module looks up the badge in the pre-compiled dictionary.

  2. Using Manually Specified BadgeIds:
    If you prefer not to use HTTPService, you can set the variable achievementService.UseHTTPService to false. In this case, the module will rely on a manually specified table of BadgeIds (achievementService.BadgeIds) to retrieve badge information.

  3. Fallback Mechanism:
    If achievementService.UseHTTPService is set to true but fetching badges via HTTPService fails, the module can optionally fall back on the achievementService.BadgeIds table by setting achievementService.BadgeIdFallback to true.

By automating or customizing badge management, AchievementService eliminates the need to manually handle badge IDs in your scripts while offering flexibility to suit your game’s needs. This is particularly helpful for games with a large number of badges or for environments where HTTPService may not always be reliable.


🔨 Installation Guide

1. Import the Module and Configure Badge Method

  • Copy the AchievementService module into your game.
  • If you want to use HTTPService (easiest method):
    • Enable Allow HTTP Requests in your game settings under the security tab.
  • If you don’t want to use HTTPService and only use BadgeIds:
    • Manually set up the achievementModule.BadgeIds table by adding your game’s BadgeIds and set the achievementModule.UseHTTPService variable to false.
  • If you want to use both (most reliable method):
    • Manually set up the achievementModule.BadgeIds table by adding your game’s BadgeIds and set the achievementModule.BadgeIdFallback variable to true.

2. Awarding Badges

  • To award an achievement to a player, simply call:
AchievementService:Award(player, badgeIdentifier) -- badgeIdentifier can either be the BadgeName or BadgeId

3. Configuration

  • AchievementService, as of v1.03, allows configuration/customization for the following:

    • Toggle animations
    • Animation Style (which type of animation)
    • Toggle sounds
    • Sound (which sound will play)
    • Speed Multiplier (how fast an animation plays)
    • Toggle haptics
    • Toggle HTTPService
    • Toggle BadgeIdFallback
    • Manually implement BadgeIds
    • Toggle Roblox default badge notification
    • Toggle automatic initialization
    • Maximum amount of retries
    • Retry Delay (the amount of delay between each retry attempt)

🧠 Example

local AchievementService = require(game.ReplicatedStorage.AchievementService)

game.Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(character)
        --[[
           You can use :Award() using the BadgeName or the BadgeId
        ]]
        AchievementService:Award(player, "Welcome")
        AchievementService:Award(player, 123456789) 
    end)
end)

📜 Update Log

v1.03 - November 29th, 2024 (Current)
  • Internal Recovery Module Added
    • Introduced :SafeCall() function to handle retries when a function fails.
    • The SafeCall function is now used within the AchievementService module for more resilient function calls.
  • New Variables
    • MaxRetryLimit
      • Controls the maximum number of retry attempts (default is 5).
    • RetryDelay
      • Defines the delay between retry attempts (default is 1 second).
  • Code Documentation Improvements
    • More comments have been added throughout the code to increase legibility and clarify the functionality for developers
  • achievementsService.Defaults Table
    • Default values for configurable variables are now stored in the Defaults table for easier management.
  • Initialization Enhancements
    • AutoInitialize variable
      • Determines whether the module initializes automatically on startup or if developers should initialize it manually.
    • Developers can now call AchievementService.init() again to retry initialization if the first attempt fails. (It is recommended to disable AutoInitialize and handle initialization yourself if you choose to do this.)
    • Improved checks during initialization for a smoother startup process.
  • Removed Redundancy
    • Removed the redundant achievementService.Timeout variable and more for cleaner and more efficient code.
  • New IntValue Dependency (Duration)
    • Added an IntValue called Duration inside each AnimationStyle folder. This new value can be used to configure the duration of animations.

This update improves both the robustness and flexibility of the AchievementService module, providing developers with more control over initialization and retry logic.


Silent Update - November 30th, 2024

  • Added some more comments :P
v1.02 - November 28th, 2024
  • Improved Code Redundancy
    • Optimized and streamlined the code for better efficiency and maintainability
  • Self-Initialization
    • The module now automatically self-initializes without the need for the user to manually call .init() . (Courtesy of @TheRealANDRO)
  • Fixed 100 Badge Limit
    • The module now handles badge pagination, ensuring that all badges are retrieved from the Roblox API, even if there are more than 100.
  • Support for BadgeName and BadgeId
    • You can now use either BadgeName or BadgeId when using :Award(), providing more flexibility.
  • Fixed Badge Description Being Nil
    • Addressed an issue where badge descriptions were sometimes returned as nil.
  • Removed Debris
    • Removed the use of Debris service. (Courtesy of @Abcreator)
  • Achievement Queue Fix
    • Fixed issues with the achievement queue that could prevent badges from being awarded correctly.
  • Added Badge Availability Check
    • Added a check to ensure that the badge being awarded is not disabled. (Courtesy of @Abcreator)
  • Toggle for Default Badge Notification
    • You can now enable or disable the Roblox default badge notification. (Courtesy of @Abcreator)

This update contains several fixes and new features, improving both flexibility and functionality! Thanks to all contributors for their valuable input!

v1.01 - November 28th, 2024
  • New Variables Added:
    • achievementService.UseHTTPService
      • Determines if AchievementService should fetch BadgeIds and BadgeInfo using HTTPService.
      • Default: true
    • achievementService.BadgeIdFallback
      • Specifies whether to fall back on achievementService.BadgeIds if HTTPService fails.
      • Default: false
    • achievementService.BadgeIds
      • An optional table for manually specifying BadgeIds when not using HTTPService.
      • Default: {} (empty table)

This update introduces configuration options to accommodate different use cases. For example, if you want to avoid relying on HTTPService, you can set achievementService.UseHTTPService to false and insert BadgeIds into the achievementService.BadgeIds table manually. Additionally, achievementService.BadgeIdFallback ensures that the system switches to using local BadgeIds when HTTPService encounters issues. Please note that the BadgeId method may take longer to fetch BadgeInfo.

v1.00 - November 28th, 2024
  • Released

📄 License

AchievementService is under the MIT license.

View license
MIT License

Copyright (c) 2024 funtmaster

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

🌐 Miscellaneous

  • Developed and created by @FuntMaster.
  • Hall of Contributors:
    @TheRealANDRO, @Abcreator
  • v1.04 plans
    • As of right now, there are no plans for v1.04. Have a suggestion? Let me know!
  • One thing to note is that AchievementService uses RoProxy to access Roblox Badge APIs. Although rare, I cannot control any downtime or slowdown that occur with this proxy. If you’d like to use an alternative, you can configure the method used to retrieve the badges (refer to the Installation Guide) or use your own self-hosted proxy.
  • Lastly, please feel free to let me know of any bugs, issues, suggestions that you have!
    P.S. this is my first time making a community resource!
24 Likes

This is a great resource and all, but is using badgeName really more efficient? I’m not a scripting professional or anything but in my knowledge calling an API especially when there could be downtime is slower than just using the Badge ID. And even if it saves time and isn’t that big of a difference, it’ll only take a few seconds to copy the ID. I feel like in most cases people would use placeholder names for badges when in-dev, and there’d be more than one of the same badge name too.

3 Likes

Upon initializing the module with AchievementService.init() , it fetches all the badges in your game using the Roblox API and compiles them into an internal dictionary. This process happens only once during initialization. When you call :Award() , the module simply looks up the badge in this pre-compiled dictionary, avoiding the need to make additional API calls. This eliminates the need to manually manage badge IDs in your scripts, which can be especially helpful if your game has a large number of badges.

1 Like

v1.01 - November 28th, 2024

  • New Variables Added:
    • achievementService.UseHTTPService
      • Determines if AchievementService should fetch BadgeIds and BadgeInfo using HTTPService.
      • Default: true
    • achievementService.BadgeIdFallback
      • Specifies whether to fall back on achievementService.BadgeIds if HTTPService fails.
      • Default: false
    • achievementService.BadgeIds
      • An optional table for manually specifying BadgeIds when not using HTTPService.
      • Default: {} (empty table)

This update introduces configuration options to accommodate different use cases. For example, if you want to avoid relying on HTTPService, you can set achievementService.UseHTTPService to false and insert BadgeIds into the achievementService.BadgeIds table manually. Additionally, achievementService.BadgeIdFallback ensures that the system switches to using local BadgeIds when HTTPService encounters issues. Please note that the BadgeId method may take longer to fetch BadgeInfo.

.init should be a function that runs automatically. Shouldn’t have to always call the .init function over and over again. Making it an optional .Reload() function would be better, in-case if the developer wants to check and automatically call .Reload if HTTPService Request fails.

1 Like

.init only needs to be called once. As a matter of fact, the function cannot be called more then once. If two or more scripts use it, only one script would be required to initialize it.

Here’s an example:

local AchievementService = require(game.ReplicatedStorage.AchievementService)

-- Initializes the service (for the entire game, not just this script)
AchievementService.init()

game.Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Connect(function(character)
        -- Example: Award achievement when player enters the game
        AchievementService:Award(player, "Welcome")
    end)
end)

In another script:

local AchievementService = require(game.ReplicatedStorage.AchievementService)

--[[
    Because the module was already initialized,
    the function does not need to be called again.
]]

game.ReplicatedStorage.RemoteEvent.OnServerEvent:Connect(function(player)
    AchievementService:Award(player, "Test Badge")
end)

Also, you wouldn’t have to initialize it every time you award a player.

1 Like

If it only has to be called once, why make it a function? Why not automatically initialize it? You’re increasing the work the developer has to do.

1 Like

The .init() function is responsible for setting up the service. It fetches all the game’s badges via HTTPService and compiles them into an internal dictionary stored within the module. This ensures that subsequent calls to :Award() can quickly reference this pre-compiled data, avoiding the need to repeatedly send API requests.

1 Like

I am not asking you what the function does. I am asking you, why do you need it?
.init is an unnecessary function when the module can initialize all the badges upon requiring.

1 Like

Ah, I see what you mean. That would make more sense. I’ll work on a fix. Thank you!

2 Likes

I took a look at this module in Studio and have a little bit of extra constructive feedback to give:

  • I noticed there is seemingly a lot of repetition between combinations when different properties are set (such as when Audio support is active with haptics vs without); it’d be helpful if you could try to reduce to amount of if comparisons so it is easier to customise how these functions work.
  • You use Debris in your script. Ideally, you should use task.delay to then :Destroy the instances since Debris could delete the instance earlier than expected if it reaches the MaxItems limit.
  • As mentioned by someone else as a reply to this post. I am curious as to why you decided to use badge names rather than badge ids for the awarding function? Badge names are perhaps easier to mess up when inputting them since you could mess up punctuation or misspell the name.
  • Following on from the above point, I can’t remember exactly how the module works so, does this module follow the general best practices for badge awarding? More specifically, the following:
    • Check the Enabled state of the badge before awarding to ensure you aren’t awarding a disabled badge.
    • Ensure the player doesn’t already own the badge because attempting to award to a player who already owns the badge will output a warning.
  • Bit of a suggestion, but it’d be nice if you could provide out-of-the-box support for BadgesNotificationsActive via a setting which would allow us to disable the default badge award notification.
  • How does the module approach error handling? Does it leave that to me, or does it retry until it works? Does it have a maximum retry limit, and does it yield the parent script? Is this something I can rely on, or is it up for change in the future?

Other than that, great module! Hope this feedback can help to improve it even further!

Init functions are incredibly helpful to prevent unexpected yields during a require. Most scripters don’t expect a require to yield, so a lot of us module creators decide to employ an Init function which handles those yielding functions as so the require itself does not need to yield. Everyone has different coding practices, of course; however, quite a few developers I’ve heard from also employ a ‘no yielding on require’ policy to prevent yeilding requires from becoming an issue in the code-base.

1 Like

Thanks for the feedback! I’ll implement these into the next update.

1 Like

Many developers would prefer yield over having to call an .init function. And this module I believe wouldn’t have any problems with yielding in scripts.

local AchievementService = require(...)
AchievementService.init() -- Since we initialize right after requiring, putting this into automation does not create a significant difference.
1 Like

As of right now, the module mostly leaves it up to the developer. However, in the initialization function, there is a fallback when HTTPService fails (which is toggle-able) that allows the developer to use their own table of BadgeIds if desired.

Though, I was thinking of implementing some sort of MaxRetryLimit and letting the module try to solve itself.

1 Like

The difference between having an init function yield and having the require itself yield is that you can wrap the init function in a coroutine without many code changes (except perhaps a variable and event to ensure you don’t try to ‘jump the gun’ on executing other functions of the module) meanwhile if you have the require itself yield; you have to manually define the type to acheive the same effect:

local module = nil::typeof(require(...)) -- we can probably do this without re-assigning the type and instead just assigning the type to module itself, but I can't trust the typechecker to like that, and I'm on mobile as of the time of writing
task.spawn(function()
    module = require(...)
end)

VS

local module = require(...)
task.spawn(function()
    module.init()
end)

I left out the variables and events that would ensure we don’t try to run a method before the module is ready because that would be the same for each version regardless. Again, it is up to preference, but in some cases, an init function ends up being more helpful; more specifically, when:

  • You don’t want your code to yield during the require and …
    • Want typechecking support without manually overriding the type.
    • Want the module itself to be able to handle the behaviour for when you may try to call a function before the module is ready.
  • You want some values in the module to be accessible at a moments notice.
1 Like

v1.02 - November 28th, 2024

  • Improved Code Redundancy
    • Optimized and streamlined the code for better efficiency and maintainability
  • Self-Initialization
    • The module now automatically self-initializes without the need for the user to manually call .init() . (Courtesy of @TheRealANDRO)
  • Fixed 100 Badge Limit
    • The module now handles badge pagination, ensuring that all badges are retrieved from the Roblox API, even if there are more than 100.
  • Support for BadgeName and BadgeId
    • You can now use either BadgeName or BadgeId when using :Award(), providing more flexibility.
  • Fixed Badge Description Being Nil
    • Addressed an issue where badge descriptions were sometimes returned as nil.
  • Removed Debris
    • Removed the use of Debris service. (Courtesy of @Abcreator)
  • Achievement Queue Fix
    • Fixed issues with the achievement queue that could prevent badges from being awarded correctly.
  • Added Badge Availability Check
    • Added a check to ensure that the badge being awarded is not disabled. (Courtesy of @Abcreator)
  • Toggle for Default Badge Notification
    • You can now enable or disable the Roblox default badge notification. (Courtesy of @Abcreator)

This update contains several fixes and new features, improving both flexibility and functionality! Thanks to all contributors for their valuable input!

1 Like

In this new update, I implemented a silent self-initialization via a coroutine. There is a variable already in place that prevents the developer from trying to call functions before the module is done initializing.

1 Like

All of these points depend on the system I am working with and does not give me a guaranteed list of benefits of having a .init function. In most cases, automatic initializiation outbenefits the manual initialization. Error handling is almost the same in both cases.

local module: typeof(require(...)) -- No need to assign nil manually
task.spawn(function()
	module = require(...)
end)
  • Does not require the developer to manually call an initializer. Thus reducing the work the developer has to do.
    • Also many developers forget there’s an initializer so they just wonder why their module isn’t working a lot of times.
  • Still supports TypeChecking.
  • Automatic initialization still makes the module behaviour stabilized.
    • A behaviour can be implemented to prevent module usage before initialization.
  • Initialization still allows for module to be accessable at a moments notice. After require, the module will be ready anyway.
1 Like

If you use a coroutine, you don’t inheritingly get typechecking unless you manually define the type afaik. That is what I was referring to.

This was more focused on variables that need to be accessed before the yield can be completed, IE: for configuration variables or a module that has some functions that rely on the initialization but others that do not.

Again, it is certainly situational and comes down down to preference. We could theoretically argue about the best practices on module requires for the next month; all I’m saying is that in these particular cases, an init function isn’t ‘unnecessary’ and can help. There is no one perfect way to handle yielding initialisations, but I do still feel the need to voice why many modules choose not to yield on require and instead implement an init function.

2 Likes

I usually don’t pay attention to open-sourced content I can make myself, but the UI you display is so appealing. Hopefully I keep it in my project, gives that old XBOX feel.

2 Likes