Every time the event's start time changes, you re-upload the world. All you want is to change one line on the notice board, and it needs a full build each time.
Put the notice text on the web and have the world go read it, and that chore disappears. Editing a text file changes the board.
This article builds a notice board that reads and displays text from the web, written so a failed load doesn't erase the previous notice.
What You'll Learn
- The request-then-receive flow
- Why you separate body from status
- Building it so the previous content survives a failure
- Why you space out the loads
Start from having tried networking basics.
There's a gap between asking and receiving
In ordinary code, a value comes back where you called for it.
int a = GetSomething(); // The result is in hand here
Reading from the web doesn't work that way. You place an order and it arrives later.

The syntax matches that shape.
// Just asks it to go fetch. Nothing is returned here
VRCStringDownloader.LoadUrl(noticeUrl, (IUdonEventReceiver)this);
The result arrives in a method called later.
| Method | When it fires |
|---|---|
OnStringLoadSuccess(IVRCStringDownload result) | It loaded. result.Result holds the text |
OnStringLoadError(IVRCStringDownload result) | It didn't. result.Error holds the reason |
Keep in mind that there are two exits, and only one of them gets taken. Holding that in mind makes the next section land easily.
URL handling matches video. What you pass is a VRCUrl, and it can't be built from a string at runtime. Put it in the Inspector, or use what a player pasted into an input field.
Also, loading is rate-limited. Repeated presses get rejected, so build in your own spacing.
Separate the body from the status
This is the design point I most want to convey here.
With only one text field on the board, you'll want to write code like this.
// Do this and the notice disappears
noticeText.text = "Loading...";
And when loading fails, the board is left showing only "Loading." The notice that was there yesterday becomes invisible to visitors.

Two fields solve it.
| Field | When it changes |
|---|---|
| Body | Only when loading succeeds |
| Status | When loading starts, succeeds, or fails |
With that, the notice stays alive even on a day with flaky connections. A small "couldn't update" appears in the status field while the notice text stays readable.
Stale information beats nothing at all by a mile. That applies well beyond notice boards, to any mechanism that fetches something from outside.
Hands-On: Build a notice board
Show text hosted on the web and make an update button re-read it.
1. Place the board
Place the following in a scene with a floor.
| Name | How to make it, and settings |
|---|---|
NoticeCanvas | UI Canvas (World Space). (0, 2, 4), Scale (0.004, 0.004, 0.004) |
NoticeText | TextMeshPro as a child of NoticeCanvas. Large text |
StatusText | TextMeshPro as a child of NoticeCanvas. Small text, at the bottom |
UpdateButton | Cube. (0, 1, 3), Scale (0.4, 0.4, 0.4) |

Put something in NoticeText up front, like "Welcome." That keeps the board from being blank before loading.
2. Prepare the text to be read
Host the text somewhere that meets these conditions.
- The text itself comes back (no page decoration or HTML mixed in)
- It's HTTPS
GitHub Gist is easy. Create one text file, publish it, and use the URL from the Raw button. Putting a .txt on GitHub Pages works too.
A file's viewing page URL won't work. That's a web page, not the text itself.
3. Write the code
Create NoticeBoard in Assets/Scripts.
using UdonSharp;
using UnityEngine;
using TMPro;
using VRC.SDK3.StringLoading;
using VRC.SDKBase;
using VRC.Udon.Common.Interfaces;
[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class NoticeBoard : UdonSharpBehaviour
{
[SerializeField] private VRCUrl noticeUrl;
[SerializeField] private TextMeshProUGUI noticeText; // Body
[SerializeField] private TextMeshProUGUI statusText; // Status
[SerializeField] private float cooldownSeconds = 10f;
private float nextAllowedTime;
private void Start()
{
Fetch();
}
// Called from the update button
public void RequestUpdate()
{
if (Time.time < nextAllowedTime)
{
SetStatus("Wait a moment and try again");
return;
}
Fetch();
}
private void Fetch()
{
nextAllowedTime = Time.time + cooldownSeconds;
SetStatus("Loading...");
// Just asks. The result arrives later
VRCStringDownloader.LoadUrl(noticeUrl, (IUdonEventReceiver)this);
}
public override void OnStringLoadSuccess(IVRCStringDownload result)
{
// The only place the body gets replaced
if (noticeText != null) noticeText.text = result.Result;
SetStatus("Updated");
}
public override void OnStringLoadError(IVRCStringDownload result)
{
// Don't touch the body
SetStatus("Couldn't update");
Debug.LogWarning("[NoticeBoard] " + result.Error);
}
private void SetStatus(string message)
{
if (statusText != null) statusText.text = message;
}
}
Two things worth reading closely.
noticeText is touched in exactly one place. That the body only changes on success is visible from a glance at the code. Expressing the design in the shape of the code makes it harder to break when you add features later.
nextAllowedTime is updated before loading starts. Updating it after success would leave no spacing on failure, allowing rapid presses.
4. Wire it up
Add a Udon Behaviour to NoticeCanvas and set NoticeBoard.
| Field | What to set |
|---|---|
| Notice Url | The direct link to your text |
| Notice Text | NoticeText |
| Status Text | StatusText |

Put a relay script on UpdateButton.
using UdonSharp;
[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class NoticeUpdateButton : UdonSharpBehaviour
{
[SerializeField] private NoticeBoard board;
public override void Interact()
{
if (board != null) board.RequestUpdate();
}
}
5. Confirm
Launch Build & Test.
| Order | Action | Expected result |
|---|---|---|
| 1 | Enter | The web text appears on the board and the status reads "Updated" |
| 2 | Edit the file on the web and press update | It changes to the new text |
| 3 | Press update rapidly | "Wait a moment and try again" appears |
| 4 | Change the URL to something nonexistent and re-enter | The body stays as the text you entered originally. Only the status says "Couldn't update" |
Row 4 is the most important check in this article. Even when loading fails, the board stays readable.
Common Pitfalls
- Nothing appears → Check the URL isn't a viewing page. You need a URL that returns the text itself
- HTML tags appear → You're reading a web page. Use a direct link to a text file
- Rapid presses stop it loading → You're hitting the rate limit. Check the spacing logic
- The characters are garbled → Set the file's encoding to UTF-8
- It works in the editor but not in a build → Check the URL permission settings. Test in an instance of your own first
- The body disappears on failure → You're touching the body inside
OnStringLoadError
Bonus: Good to Know Up Front
- Each person loads it themselves: One person pressing update doesn't change anyone else's board. To line everyone up, send the fetch trigger to everyone with network events
- A delimiter lets you split fields: Decide that line one is a title and the rest is the body, and
Split()puts them in separate fields. JSON works when you want more complexity, though lines suffice at first - Test with a real URL, even in the editor: Local file paths can't be read. Confirm with a published URL
- Keep the text short: Notice-board volumes are fine; don't build something that loads tens of thousands of characters
- It's useful all over: Event schedules, rules, changelogs, a daily message. Anything you want to change without rebuilding
Summary
Reading external text takes the shape of asking and receiving later.
LoadUrl()only asks. The result arrives through one of two exits, success or failure- Replace the body only on success. Don't erase the notice on failure
- Put status in a separate field
- Space out the loads yourself
The question to ask while building is: "When it can't load, what does a visitor see?" If they still see the previous notice, the design is right.
To display external images, go to swapping posters with external images. To play video, go to setting up a video player.