I Build World-Class Products

I Make Delightful Sites Apps Games

Open Source

Apple Swift Student Challenge

I am incredibly honored to announce that I have won Swift Student Challenge again. This time as a distinguished winner. I am happy to open source my project Oragnize. Please get in touch if you have any question.

Email Me

United Safety Video

My arse has been bolted to a fair share of different airlines. But United’s safety video irritates me the most.

You can’t cut the video or add CG if you are gonna do a Rube Goldberg machine.

What pushed me to fly United more frequently was Southwest policy changes that made them way less special for me. And United’s frequent SFO-LAX connection. Their app and site have some genuinely handy features not found elsewhere. And most importantly, fast free Wi-Fi for everyone thanks to Starlink. They have improved their service by quite a bit compared to a decade ago.

Starlink has been a highlight of my trip at Sequoia last Christmas too. It pairs well with travel.

Update On The Blog - Part 2

Wow, it’s been a while. Time and life have been moving incredibly fast for the last 2–3 years for me since the start of this blog. Somehow, in the past year, it managed to move even faster. That’s the biggest part I’ve been away from this for almost exactly one year. Another factor was my chronic procrastination and habit of not paying bills on time, which took down the blog and the project section of the site. But alas, I am back.

But I think there is a deeper layer to this. Ever since I ran out of my maximum allowed work authorization while at school half a year ago ( among other things), I managed to catch a nice break. I could have returned to this earlier, but I guess I wasn’t in the mood. I think now, though, I am on the other end of a very slow burnout. And I am finding my motivation for writing, reading, and programming making a return.

I’ve never been consistent at writing, but periodically I have good streaks. Maybe this could be the start of one of them.

Life aside, I am glad to say that this site is getting the much-needed update and maintenance. I finally fixed the /project a year after it went down. The /read section is getting an overhaul; now it properly showcases what I love to read. The blog section also gets some changes. You can now see the full text of each entry without needing to click in. How it should be in the first place.

Under the hood, big changes are made that move the blog’s content management system (CMS) from Ghost to one custom-made for this website. I did it to make the blog less fragile, more maintainable, while still allowing me to use my favorite writing tools. For those who care, I basically made my custom CMS api in parity with Ghost’s API.

Another big change is you can now follow this blog on the fediverse @yuhao@yuhao.app; the support is still very experimental. It is implemented thanks to fedify.

I have a lot more planned for this blog and this site; after all, it is meant to be a place that holds all things digital for me.

Peeker’s Advantage in Asymmetrical Network Latency

Peeker’s advantage is the edge a moving player (P) gains when swinging into line of sight against a holding player (H). It exists in all server-authoritative shooters due to latency. A common claim is that peeker’s advantage depends only on the holder’s connection [1]. That is correct only if the peeker’s latency is uniform. If the peeker’s uplink differs between “peek” and “shot,” the advantage grows.

Mettler, Tang, and Chen [2] provided more detailed explanation of Peeker’s Advantage in symmetrical network conditions.

[1] Peeker's advantage in symmetry

Setup

One server S, two clients:

  • Holder H: uplink uₕ, downlink dₕ, round-trip time Lₕ = uₕ + dₕ.
  • Peeker P: uplink while revealing Lps (“peek packet”), uplink while firing Lpa (“shot packet”).

Both have the same human reaction time R. The server resolves hits. First valid shot at the server decides the duel.

Peeker's Advantage

t1 => P swings into sight.

t2 => P’s reveal reaches S afterLps and reaches H after Lps + dₕ. H first sees P at this moment.

t3 => H reacts forR. H’s shot then travels uplink uₕ. So H’s shot arrives at server at

  tH = Lps + dₕ + R + uₕ.

t4 => P sees H instantly at swing time. P reacts for R. P’s shot travels uplink Lpa. So P’s shot arrives at server at

  tP = R + Lpa.

Peeker’s Advantage Formula

Define the advantage D as the time gap between H’s and P’s first shots arriving at the server:

  D = tH – tP.

Case 1. Uniform Peeker Latency

If peek and shot uplinks are equal (Lps = Lpa):

  D = (Lps + dₕ + R + uₕ) – (R + Lps)

  = uₕ + dₕ

  = Lₕ.

So with uniform latency, the advantage equals the holder’s round-trip. This is the source of the saying “only your ping matters.”

Case 2. Asymmetrical Peeker Uplink

If peeker’s uplink while revealing is higher than while firing, define the delta

  Lpd = Lps – Lpa > 0.

Plugging in:

  D = (uₕ + dₕ) + (Lps – Lpa)

  = Lₕ + Lpd.

Result

  • Uniform peeker latency: advantage = Lₕ.
  • Asymmetrical uplink (Lps > Lpa): advantage = Lₕ + Lpd.

In words: the peeker gains not only the holder’s round-trip but also the extra head start equal to the difference between peek uplink and shot uplink.

Peeker's advantage in asymmetry

Next Steps

  • Empirical validation/simulation
  • We examined Lps – Lpa > 0 grows peeker's advantage. Naturally Lps + Lpa > 0 shrink peeker's advantage. How about that! Maybe peeker's advantage does have a solution after all (with serious tradeoff ofc).
  • People has been using network throttle to cheat for ages, maybe it's time for intelligent network throttle. Maybe they already exist.
  • How to mitigate and defend against such cheat.

References

[1] M. deWet and D. Straily, “Peeking into VALORANT’s netcode,” Riot Games Technology, Jul. 28, 2020. [Online]. Available: https://technology.riotgames.com/news/peeking-valorants-netcode

[2] C. Mettler, D. Tang, and Z. Chen, A study on the effects of network latency on peeker’s advantage in FPS games, Interactive Qualifying Project Report, Worcester Polytechnic Institute, 2023. [Online]. Available: https://web.cs.wpi.edu/~claypool/iqp/fps-lag-22/final-report.pdf

Citing This

@article{chen2025peeker,
  title   = {Peeker’s Advantage in Asymmetrical Network Latency},
  author  = {Chen, Yuhao and Yang, Tianchi},
  year    = {2025}
}

An Update On The Blog

Hi, this is Yuhao. Due to a billing oversight, the DigitalOcean instance hosting the blog CMS was deleted. Unfortunately, the Coolify backup was not set up correctly, so the restoration process has taken longer than expected, and the available backup is incomplete.

Thankfully, I still have the drafts for all published posts on yuhao.app/blog, and I’ll be manually restoring them over the coming days.

I sincerely apologize for this oversight and appreciate your patience as I work to bring everything back online.

Pointers and References in C++

This summer, I am brushing up my C++ by going through Alex's excellent tutorial at learncpp.com. I recently went through its chapter on Pointers and References, and I cleared a lot of my previous confusion on the topic. Here are some notes I took.

References in C++

int        // a normal int type (not an reference)
int&       // an lvalue reference to an int object
const int& // a constant lvalue reference

Both T& ref and T &ref are valid ways to define a reference but the first one is preferred.

int x {5};
int& ref {x};

ref = 6;
std::cout << x << '\n'; // 6

Modify a reference will modify the object it refers too

int x {5};
int& ref {x};

int y {6};
ref = y;
std::cout << x << '\n'; // 6

You can not reseat a reference

const int x {5};
// int& ref {x}; <-- this is not allowed
const int& ref {x}; // this is allowed and preferred way to reference object, unless you explicitly need to modify a value through reference.

const T& also can be init with literal (or other rvalue), for example, const int& {5} where as T& couldn’t. Note that 5, a temporary value, it's lifetime is extended by const T&

T& const T&
Modifiable Lvalue ✅ ✅
Rvalue ⛔️ ✅
Constant Lvalue ⛔️ ✅
Can modify original object ✅ ⛔️

What the hell is l-value and r-value?

int x { 5 }; // 5 is an rvalue expression
int y { x }; // x is an lvalue expression
const double d{ 1.2 }; // 1.2 is an rvalue expression
const double e { d }; // d is a non-modifiable lvalue expression
  1. More formal explanation:
    • Lvalue expressions evaluate to an identifiable object.
    • Rvalue expressions evaluate to a value.
  2. More straightforward (to me) explanation:
    • A lvalue that stands for locate value is a variable that holds a value in memory
    • A rvalue is a variable itself and that it has no memory allocated.

Pointers in C++

Dereference operator *

#include <iostream>

int main()
{
    int x{ 5 };
    std::cout << x << '\n';  // print the value of variable x
    std::cout << &x << '\n'; // print the memory address of variable x

    std::cout << *(&x) << '\n'; // print the value at the memory address of variable x (parentheses not required, but make it easier to read)

    return 0;
}

// 5
// 0027FEA0
// 5

Pointers

A pointer can be declared via T* x, and a pointer declared but not initialized is called a wild pointer. You can initialize a pointer via T* {&x}.

Assignment in pointer

We can use assignment with pointers in two different ways:

  1. *ptr = 6; To change what the pointer is pointing at (by assigning the pointer a new address)
  2. ptr = &x; To change the value being pointed at (by assigning the dereferenced pointer a new value)
#include <iostream>

int main()
{
    int x{ 5 };'
    int* ptr{ &x }; // ptr initialized to point at x

    std::cout << *ptr << '\n'; // print the value at the address being pointed to (x's address)

    int y{ 6 };
    ptr = &y; // // change ptr to point at y

    std::cout << *ptr << '\n'; // print the value at the address being pointed to (y's address)

    return 0;
}
  • &x return a pointer not the literal address to x
  • On a 32 bit architecture, a pointer is 4 bits

Null Pointer

When you initialize a pointer like int* ptr{}, you get a null pointer.

You can also assign a pointer with ptr = nullptr to assign it with null pointer.

You **can but should avoid **initialize a nullptr with T* ptr{0}; or T* ptr{NULL};.

You can test whether a pointer is a null pointer by if (ptr == nullptr) or just if (ptr).

  • Note: Testing whether a pointer is not a null pointer does not alway guarantee a pointer is safe to deference. A dangling pointer to a destroyed item does not automatically set to nullptr. Therefore it’s your responsibility to detect these cases and ensure those pointer are set to nullptr.

Pointers and const

const int x {5};
int* ptr { &x }; // compile error: cannot convert from const int*

You cannot set a normal pointer to a pointer at a const variable.

Const pointers

A const pointer Is a pointer whose address cannot be changed — T* const ptr {&x};.

You can dereference *ptr and modify the original object x given x is a non constant.

Const Const pointer to a const value

You can declare by const T* const ptr { &x }; even if T x is a non const. In this case the nether the pointer accept assignment nor dereferenced pointer accept assignment.

WTH? Here is a cheat sheet

int x {5} const x {5}
int* ptr {&x} Pointer and dereferenced pointer accept assignment Declaration not allowed
const int* ptr {&x} Pointer accept assignment but dereferenced pointer don’t Pointer accept assignment but dereferenced pointer don’t
int* const ptr {&x} Dereferenced  pointer accept assignment but pointer don’t Declaration not allowed
const int* const ptr {&x} Neither pointer nor dereferenced pointer accept assignment Neither pointer nor dereferenced pointer accept assignment

Even simpler cheat sheet

Declaration**** int x const int x
int* ptr = &x *ptr = ... ✅ptr = ... ✅ 🚫 illegal declaration
const int* ptr = &x *ptr = ... ❌ptr = ... ✅ *ptr = ... ❌ptr = ... ✅
int* const ptr = &x *ptr = ... ✅ptr = ... ❌ 🚫 illegal declaration
const int* const ptr = &x *ptr = ... ❌ptr = ... ❌ *ptr = ... ❌ptr = ... ❌
  • *ptr = ... → assigning to the pointee
  • ptr = ... → reassigning the pointer itself
  • ✅ = assignment allowed
  • ❌ = assignment not allowed
  • 🚫 = declaration is invalid (compile error)

Pass by Reference and Pass by Address

Passy by reference

void addOne(int& y) // y is bound to the actual object x
{
    ++y; // this modifies the actual object x
}

// int x{5};
// addOne(x);
#include <iostream>

void printAddresses(int val, int& ref)
{
    std::cout << "The address of the value parameter is: " << &val << '\n';
    std::cout << "The address of the reference parameter is: " << &ref << '\n';
}

int main()
{
    int x { 5 };
    std::cout << "The address of x is: " << &x << '\n';
    printAddresses(x, x);

    return 0;
}

// The address of x is: 0x7ffd16574de0
// The address of the value parameter is: 0x7ffd16574de4
// The address of the reference parameter is: 0x7ffd16574de0

T funcName(T& x) and T funcName(const T& x) follow pretty much same rule as const T& and T&, a reference to non-const (which can only bind to modifiable lvalues), a reference to const can bind to modifiable lvalues, non-modifiable lvalues, and rvalues. But value modification is not allowed for T funcName(const T& x).

Pass by Address

void addOne(int* y) // y is bound to the actual object x
{
    ++y; // this modifies the actual object x
}

int x{5};
addOne(&x);

Pass by value? By reference? And by address

Feature**** Pass by Value**** Pass by Reference**** Pass by Address****
Syntax**** void foo(int x) void foo(int& x) void foo(int* x)
Argument Type**** Copy of the actual value Alias to the original variable Pointer to the original variable
Called with**** foo(a) foo(a) foo(&a)
Can modify original?**** ❌ No ✅ Yes ✅ Yes (via dereferencing)
Memory Use**** More (creates a copy) Less (no copy) Less (only pointer is copied)
Null Safety**** Always safe Always safe ❌ Must check for nullptr
Typical Use Cases**** When you don’t want to modify input When you want to modify input When dealing with dynamic memory / APIs
Example Call**** foo(5); foo(a); foo(&a);
Inside Function Access**** Use as-is (x) Use as-is (x) Dereference (*x)
  • Note:
    **Prefer void foo(T* const value) over void foo(T* value). **And yes you canvoid foo(const T* const value) and void foo(const T* const value), it follows the same rule referenced in the cheat sheet.

When to use pass by value vs pass by reference

  • Fundamental types and enumerated types are cheap to copy, so they are typically passed by value.
  • Class types can be expensive to copy (sometimes significantly so), so they are typically passed by const reference.
    Actually got asked this question in a Tesla interview

Pass by reference vs by address

Pass by const reference has a few other advantages over pass by address.

First, because an object being passed by address must have an address, only lvalues can be passed by address (as rvalues don’t have addresses). Pass by const reference is more flexible, as it can accept lvalues and rvalues.

Second, the syntax for pass by reference is natural, as we can just pass in literals or objects. With pass by address, our code ends up littered with ampersands (&) and asterisks (*).

In modern C++, most things that can be done with pass by address are better accomplished through other methods. Follow this common maxim: “Pass by reference when you can, pass by address when you must”.

Pass by address copies the address

// Consider
void foo(int* ptr)
{
    ptr = nullptr;
}

int x{ 5};
int* ptr{ &x };
foo(x);

std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n"); // ptr is non-null

Therefore reassign a passed by pointer to another address does not reassign the original pointer (because foo() copies reference).

Pass by address by reference

Instead, if you want achieve above, you can:

void bar(int*& ptr)
{
    ptr = nullptr;
}

Return by reference and address

Similar to pass by value, return by value also returns a copy of the object:

std::string returnByValue();
#include <iostream>
#include <string>

const std::string& getProgramName() // returns a const reference
{
    static const std::string s_programName { "Calculator" }; // has static duration, destroyed at end of program

    return s_programName;
}

int main()
{
    std::cout << "This program is named " << getProgramName();

    return 0;
}

// This program is named Calculator

Because getProgramName() returns a const reference, when the line return s_programName is executed, getProgramName() will return a const reference to s_programName (thus avoiding making a copy). That const reference can then be used by the caller to access the value of s_programName, which is printed.

**Using return by reference has one major caveat: the programmer must be sure that the object being referenced outlives the function returning the reference. **Otherwise, the reference being returned will be left dangling (referencing an object that has been destroyed), and use of that reference will result in undefined behavior.


const std::string& getProgramName()
{
    const std::string programName { "Calculator" }; // now a non-static local variable, destroyed when function ends

    return programName;
}

This will result in previous program undefined since programName is now destroyed at the end of the function.

Lifetime extension doesn’t work across function boundaries

#include <iostream>

const int& returnByConstReference(const int& ref)
{
    return ref;
}

int main()
{
    // case 1: direct binding
    const int& ref1 { 5 }; // extends lifetime
    std::cout << ref1 << '\n'; // okay

    // case 2: indirect binding
    const int& ref2 { returnByConstReference(5) }; // binds to dangling reference
    std::cout << ref2 << '\n'; // undefined behavior

    return 0;
}

In this case, a temporary object is created to hold value 5, which function parameter ref binds to. The function just returns this reference back to the caller, which then uses the reference to initialize ref2. Because this is not a direct binding to the temporary object (as the reference was bounced through a function), lifetime extension doesn’t apply. This leaves ref2 dangling, and its subsequent use is undefined behavior.

Don’t return non-const static local variables by reference

You are allowed to do so but will often result in unintended behavior:

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 }; // note: variable is non-const
    ++s_x; // generate the next id
    return s_x; // and return a reference to it
}

int main()
{
    const int& id1 { getNextId() }; // id1 is a reference
    const int& id2 { getNextId() }; // id2 is a reference

    std::cout << id1 << id2 << '\n';

    return 0;
}

Assigning/initializing a normal variable with a returned reference makes a copy

If a function returns a reference, and that reference is used to initialize or assign to a non-reference variable, the return value will be copied (as if it had been returned by value).

#include <iostream>
#include <string>

const int& getNextId()
{
    static int s_x{ 0 };
    ++s_x;
    return s_x;
}

int main()
{
    const int id1 { getNextId() }; // id1 is a normal variable now and receives a copy of the value returned by reference from getNextId()
    const int id2 { getNextId() }; // id2 is a normal variable now and receives a copy of the value returned by reference from getNextId()

    std::cout << id1 << id2 << '\n';

    return 0;
}

// 12

It’s okay to return reference parameters by reference

#include <iostream>
#include <string>

// Takes two std::string objects, returns the one that comes first alphabetically
const std::string& firstAlphabetical(const std::string& a, const std::string& b)
{
	return (a < b) ? a : b; // We can use operator< on std::string to determine which comes first alphabetically
}

int main()
{
	std::string hello { "Hello" };
	std::string world { "World" };

	std::cout << firstAlphabetical(hello, world) << '\n';

	return 0;
}

// Hello

It’s okay for an rvalue passed by const reference to be returned by const reference

When an argument for a const reference parameter is an rvalue, it’s still okay to return that parameter by const reference.
This is because rvalues are not destroyed until the end of the full expression in which they are created.

#include <iostream>
#include <string>

const std::string& foo(const std::string& s)
{
    return s;
}

std::string getHello()
{
    return "Hello"; // implicit conversion to std::string
}

int main()
{
    const std::string s{ foo(getHello()) };

    std::cout << s;

    return 0;
}

The caller can modify values through the reference

This is very confusing behavior. Hence imho, make you return const *

#include <iostream>

// takes two integers by non-const reference, and returns the greater by reference
int& max(int& x, int& y)
{
    return (x > y) ? x : y;
}

int main()
{
    int a{ 5 };
    int b{ 6 };

    max(a, b) = 7; // sets the greater of a or b to 7

    std::cout << a << b << '\n';

    return 0;
}

Return By Address

Return by address works almost identically to return by reference, except a pointer to an object is returned instead of a reference to an object. Return by address has the same primary caveat as return by reference -- the object being returned by address must outlive the scope of the function returning the address, otherwise the caller will receive a dangling pointer.

The major advantage of return by address over return by reference is that we can have the function return nullptr if there is no valid object to return. For example, let’s say we have a list of students that we want to search. If we find the student we are looking for in the list, we can return a pointer to the object representing the matching student. If we don’t find any students matching, we can return nullptr to indicate a matching student object was not found.
The major disadvantage of return by address is that the caller has to remember to do a nullptr check before dereferencing the return value, otherwise a null pointer dereference may occur and undefined behavior will result. Because of this danger, return by reference should be preferred over return by address unless the ability to return “no object” is needed.

WTH Sheet, again

const std::string& foo(const std::string& s) {
    return s;
}
Code**** Safe?**** Why****
const std::string s{ foo("Hello") }; ✅ "Hello" is converted to a temporary std::string. foo() returns a reference to it, and s is copy-constructed from it before the temporary dies.
const std::string**&** s{ foo("Hello") }; ❌ "Hello" becomes a temporary std::string, bound inside foo, but the returned reference points to that temporary, which dies after the full expression → ref is dangling****
std::string temp = "Hello"; const std::string& ref = foo(temp); ✅ temp is a named variable. foo(temp) returns a reference to it, which is safely bound to ref — no temporary, no lifetime issue****
const std::string& ref = "Hello"; ✅ "Hello" is promoted to a temporary std::string, but since it is directly bound to a const reference, the lifetime is extended — valid

In short const std::string s{ foo("Hello") }; good as it make a copy before temporary dies.

const std::string& s{ foo("Hello") }; bad because temporary will die after the full expression. The difference is & if you still couldn’t tell.

★Making of LetgO 🎈

Recently, a team I contributed to as a minor collaborator - as interaction designer -submitted a game to the thatgamecompany × COREBLAZER GAME JAM 2025. The director of the game is my friend Ebay. I first met him through Cooper and Justin, who worked with Ebay on a different project at USC — a game called Myth of Lumi.

Ebay later became my Student Assistant in CTIN 534, a class taught by Peter Brinson. One day, I happened to run into him and we ended up having a long conversation. That’s when he pitched me this new game — a journey about a balloon.


The game is called Balloon. You play as a little red balloon floating through the air. You’re light, fragile, and can’t really control where you’re going—just drifting with the wind. All you can do is gently pull on the string to try and steer. The heart of the game lies in how you interact with the people you meet along the way. Like in real life, different people respond differently to hope: a kid might run toward you with joy, a tired office worker might glance up and smile, and a little girl might kick you like a soccer ball. The balloon reflects the people it passes — it’s a simple, poetic way to explore how people feel about hope itself. The whole experience plays out like a quiet, interactive poem.


When I first heard the pitch, it immediately reminded me of Flower by Jenova Chen — one of the more well-known game designers to come out of USC. Fittingly, Ebay later told me the game received a lot of mentorship and feedback from Tracy Fullerton, who mentored Jenova back in the day.

I immediately expressed interest in working on the project — but instead of taking on my usual role as an engineer, I told Ebay I was more interested in helping design the interactions. Partly because the concept really resonated with me, and partly because the engineering side was already in very good hands. The team had some incredibly talented developers, led by Shuyang — a graphics specialist and recent USC grad who also worked on Myth of Lumi.

Working with Ebay on this project taught me so much, especially because of his incredible attention to detail. He really focused on the finer points, like the exact background setting and the era of the game. We spent a lot of time finding that perfect balance between making the world relatable to a modern audience while also giving it a kind of timeless, almost nostalgic feel.

As an interaction designer, my job was to create detailed interaction design documents. These included everything from how characters look — their height, their clothing, their overall appearance — so that artists could turn those ideas into concept art and eventually 3D models. I also had to document the behaviors of each character or NPC — like what they do when they’re idle, or how they react when they see the balloon. Are they going to jump up and try to grab it? Are they going to tie a message to it and send it along to someone else? All of these details had to be described clearly so that the engineers could bring them to life in code.

But the most important part of this process was thinking about the characters’ backstories and, ultimately, their relationship to hope. Each character’s attitude toward hope shapes how they respond to the balloon, and that’s really what makes the game feel like a living, breathing poem.

But the real heroes of this project were the artists, engineers, and especially my friend Justin, who worked as the level designer. In just about a month, the artists created these incredibly detailed 3D characters with a super charming art style, full of life and personality. The engineers managed to implement a ton of features that brought the story to life, packing all these emotional highs and lows into a simple 15-minute experience. We had moments where the wind would push the balloon, characters would grab it and run off, and obstacles would pop up, all making the game feel dynamic and engaging.

Justin, our level designer, really thought through the emotional journey we wanted players to experience. He focused on how to teach players to interact with the balloon naturally, making sure they understood the mechanics and could enjoy the game fully. And I can’t forget the amazing musicians who created the score that tied everything together and gave the game its poetic heart.


Of course, none of this would have happened without Ebay, who worked tirelessly — sometimes with barely any sleep — to bring it all together. And, impressively, he managed all this while also working as a translator with me during the Seasun Games visit to USC. I’m incredibly proud of what our team created for the game jam.

Update: We ended up receive a nomination from the game jam and the team is invited to see Jenova Chen. Unfortunately, I wasn't able to meet him as I wasn't in China during the time. But here are some fun pictures from my good friend Justin Huang:

Members that was able to travel to Shanghai
Justin's "holy artifact"

Translating Disney

A week ago I got paid to go to Disneyland. I was invited to be a translator for a group of executives from Amazing Seasun Games who were visiting USC for a two-week summer program. Professor David White, their guide and video game producer and veteran, has been to the park over 300 times. Me on the other hand, have only visited Disneyland and California Adventure once with family. So this turned out to be quite a learning experience for me as well, and here are a few notes I took.

Disneyland isn’t just a theme park. It’s structured storytelling. Every object, path, and landmark is placed intentionally. David pointed out the “Weenie” — a visual magnet, like the castle, that pulls guests through space. It’s basic spatial psychology, but executed at scale.

The original park was built at a smaller scale on purpose. Doors, windows, fences—slightly undersized so children feel like they are grownups. Quite cool!

Even old rides like Snow White are built narratively. The queue introduces characters. The ride pays it off. It’s a common story structure—setup, delivery, resolution. But the ride that really demonstrates that is Rise of the Resistance in Star Wars: Galaxy's Edge. Rise of the Resistance is both a technological marvel and a storytelling masterpiece. It employs almost all narrative techniques as well as technologies available to themed entertainment industry.

Underneath Sleeping Beauty Castle is a special spot—where Walt Disney himself once stood.

Walt Disney love trains, and that's why there are so many trains in the park. He also had a model train built at the backyard of his home

You can buy a giant turkey leg in the park, and it's one of the juciest turkey I had (they usually are so dry).

★Villains

Aaron Sorkin, when asked how to portray a villain characters, says

"You (the author) cannot think of them as a villain. You can't judge the character… You have to be able to empathize with that character. Find something about that character that is like you too. And you have to write that character like they're making their case to God why they should be allowed to heaven. "

There was an attack last weekend at the No King’s protest in Salt Lake City, Utah, where a bystander—fashion designer Arthur Foals An Loo—was shot and killed. The incident received little attention because, over the same weekend, a man named Vance Luther Boelter assassinated Minnesota legislator Melissa Hortman and her husband. Earlier that morning, Boelter had also shot and critically injured State Senator John Hoffman and his wife. Boelter also killed Hortman's family rescued dog Gilbert — a golden retriever, who was ‘too happy-go-lucky’ to be a service dog.

Mike Lee, a U.S. Senator from Utah, tweeted, “This is what happens. When Marxists don’t get their way,” baselessly suggesting that Boelter was a Marxist. However, as many people—including Paul Graham—have pointed out, Boelter was actually a conservative who voted for Trump in the last election.

I find it very hard to portray Senator Lee and his friends as villains.

★Lessons from The Nest

I recently went to The Nest — an immersive theater experience by Hatch Escapes — with a couple of friends. If you're around LA, I highly recommend it; the experience is both immersive and intimate.

I won't spoil anything, but one of the best moments from the experience was picking up the phone and talking to one of the characters. In a barely lit room filled with detailed props, I was truly transported to the fictional world crafted by the creators. Rest assured, though, The Nest isn't a scary experience at all; it has nothing to do with horror. Nor is it a difficult experience. Right from the start, the staffer told us the experience only has light puzzle elements that mostly serve to enhance the narrative.

But what's most intriguing to me is that its story is actually quite straightforward — no major twists or turns, no complicated plot devices. What the story truly excels at is clearly conveying a simple narrative to the player through innovative means.

As an inexperienced writer, one of my most common mistake is trying to make a story novel but ended up making it complicated. I am fortunate enough to attend writing classes of many great movie/game writers at USC. Many of them say every story is a remix of a handful of stores. What this means to me is that one should tell the simplest version of a story and let the audience fill in the gaps.

Circling back to The Nest, it's an experience designed to last around 90 minutes, with the majority of that time dedicated to players solving puzzles. I'd estimate only about 30 minutes are actually spent on storytelling. Knowing this constraint, the creators wrote a story with the simplest construct. However, by placing this straightforward narrative within a well-crafted and detailed set, the team understood that this was more than enough to truly immerse players.

Super Hero Movies

I went to watch Thunderbolt* (2025) tonight and it made me excited about what’s next in MCU. I very much look forward to The Fantastic Four and the Avengers film coming next.

陳宇昊主理

© 2026 Yuhao. All Rights Reserved. Made with love.

Privacy