[VRChat] Updating a Board from External Text: Keep the Old Notice on Failure

Created: 2026-09-08Last updated: 2026-09-09

Displaying text from the web on a world's notice board. Covers the request-then-receive flow, why you separate body from status, and building it so a failed load doesn't erase the notice.

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.

Text from the web arriving on a world's notice board

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.

Sponsored


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.

Request the load; either success or failure arrives afterwards

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.

MethodWhen 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.

Sponsored

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.

Replace the body only on success; put status in a separate field

Two fields solve it.

FieldWhen it changes
BodyOnly when loading succeeds
StatusWhen 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.

NameHow to make it, and settings
NoticeCanvasUI Canvas (World Space). (0, 2, 4), Scale (0.004, 0.004, 0.004)
NoticeTextTextMeshPro as a child of NoticeCanvas. Large text
StatusTextTextMeshPro as a child of NoticeCanvas. Small text, at the bottom
UpdateButtonCube. (0, 1, 3), Scale (0.4, 0.4, 0.4)
Board layout with body and status separated

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.

FieldWhat to set
Notice UrlThe direct link to your text
Notice TextNoticeText
Status TextStatusText
Body and status going into separate fields

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.

OrderActionExpected result
1EnterThe web text appears on the board and the status reads "Updated"
2Edit the file on the web and press updateIt changes to the new text
3Press update rapidly"Wait a moment and try again" appears
4Change the URL to something nonexistent and re-enterThe 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.

Sponsored

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.

VRChat Notes in this section63