Concept Sketchbook · 03 · Patience and reveal

Fishboy.

鱼男孩

An interactive piece about memory, childhood and the quiet metaphors of the sea.

Date
July 18, 2025
Input
Click the same spot six times, then click the fish
Built with
p5.js · OpenProcessing
Status
Sketch · playable
Live sketch · Click the same spot six times, then click the fishOpen on OpenProcessing ↗
(01)

Exploration

The feeling · Patience and reveal

Can patience be the mechanic?

Clicking anywhere gives a fragment. Only staying in one place makes something whole. The player has to choose to return to the same spot, again and again, which is also how memory works.

(02)

Narrative exploration

A child catches a fish by the sea and hides it in his belly. The fish learns to breathe on land, and the child slowly can’t.

The story is ten short lines, in Chinese and English. Each time a finished fish is clicked, the next line appears, in order.

The last lines turn the story around. The boy is the one in the water now, looking up, and the fish sits with his mother.

Point of view

The story is told in the first person by the boy, as an adult looking back. The player is not the boy. The player is the one who makes the fish, and so the one who lets him remember.

Narrative structure

Ten lines, in order, like a short memoir told one sentence at a time. Each line costs six patient clicks, so the reader can’t skim. After the last line the fish keep whispering random lines, the way a memory replays long after it has been told.

Motifs

The fish

A secret kept inside the body. Something that belongs to the sea, carried onto land.

The mother

The one who said let it go, and the one the fish ends up with.

The fish tank

The reversal. The boy who took the sea inside ends up inside the glass himself.

Emotional arc
  1. 01

    Scattered

    Early clicks leave fragments everywhere. The screen looks like a half-remembered childhood.

  2. 02

    Holding still

    The player learns to stay in one place, and the first fish appears. The story begins.

  3. 03

    Reversal

    The last lines arrive: the boy in the tank, the fish with his mother. The fragments on screen now read as everything he lost.

Mechanic as meaning
ActionMeaning
Returning to one spot→Going back to a memory
Six clicks→A memory takes time to form
Fragments left behind→Attempts that didn’t become anything
Lines in order→A childhood told from the beginning
(03)

Result and iteration

What came out of it

Patience became visible. The screen fills with fragments from every time the player wandered, and only a few whole fish. The feeling that came out of it is the slowness of remembering.

Lines from the sketch
When I was a child, I caught a fish by the sea.小时候,我在海边抓到了一条鱼。
My mother was frightened and told me to let it go.母亲很害怕,叫我放掉它。
I secretly hid the fish in my belly.我偷偷地把鱼藏在肚子里。
Later, it learned to breathe, while I gradually could not adapt to life on land.后来,它学会了呼吸,而我,渐渐无法适应在陆地的生活。
Now, it sits with my mother.现在,它和母亲坐在一起。
And I turned over in the fish tank, all alone.而我,在鱼缸里寂寞地翻了个身。
Decisions in the code

Six clicks

Few enough to finish, many enough that it feels like staying.

A 30 px circle

The six clicks must fall within 30 px of the first one. Close, not exact, so a hand can manage it.

Lines in order, then at random

The ten lines play in sequence. After the last, fish return random lines, like a memory replaying.

(04)

Code

Structure
story[]
Ten [Chinese, English] pairs, read with storyIndex.
Blob
Soft, noise-shaped background forms that drift slowly.
FishPart
The fragment left by a click that didn’t complete a fish.
CompleteFish
A finished fish, with isClicked() to tell the story.
mousePressed()
Checks fish first, then keeps the last six clicks and tests whether they all fall in one place.
mousePressed(): six clicks make a fishp5.js
function mousePressed() {
  // clicking a finished fish tells the next line of the story
  for (let f of completeFishes) {
    if (f.isClicked(mouseX, mouseY)) {
      let [cn, en] = storyIndex < story.length
        ? story[storyIndex++]
        : random(story);
      currentText = cn + "\n" + en;
      return;
    }
  }

  // remember the last six clicks
  clickHistory.push(createVector(mouseX, mouseY));
  if (clickHistory.length > 6) clickHistory.shift();

  // six clicks within 30 px of each other become a fish
  if (clickHistory.length === 6) {
    let consistent = clickHistory.every(v =>
      dist(v.x, v.y, clickHistory[0].x, clickHistory[0].y) < 30);
    if (consistent) {
      completeFishes.push(new CompleteFish(mouseX, mouseY));
      clickHistory = [];
      return;
    }
  }

  // otherwise, only a fragment is left behind
  fishParts.push(new FishPart(mouseX, mouseY));
}

Lightly condensed from the original for reading. The whole mechanic is a six-item memory of where the player clicked.

Full source on OpenProcessing ↗The embedded sketch above also has a Code tab with the complete source.

(05)

Method

What this sketch taught me about the method
01

Make waiting visible

Fragments stay on screen, so time spent is never lost.

02

Forgive the hand

A 30 px tolerance asks for intention, not precision.

03

Pay out story slowly

One line per fish keeps the narrative tied to the effort.

See the method behind the series →

概念速写本 · 03 · 耐心与揭示

鱼男孩.

Fishboy

一件关于记忆、母亲与童年隐喻的互动作品。

日期
2025 年 7 月 18 日
操作
在同一个地方点击六次,再点击鱼
工具
p5.js · OpenProcessing
状态
速写 · 可试玩
在线试玩 · 在同一个地方点击六次,再点击鱼在 OpenProcessing 打开 ↗
(01)

探索

感受 · 耐心与揭示

耐心本身能不能就是玩法?

随便点一下,只会得到一个碎片;只有守在同一个地方,才能拼出完整的东西。玩家得选择一次又一次回到同一个位置,记忆也是这样运作的。

(02)

叙事探索

一个孩子在海边抓到一条鱼,把它藏进了肚子里。鱼学会了在陆地上呼吸,孩子却渐渐不能。

故事是十句短句,中英对照。每点一次完整的鱼,就按顺序出现下一句。

最后几句把故事翻了过来:现在在水里抬头往上看的是男孩,鱼和母亲坐在一起。

视角

故事由男孩以第一人称讲述,是长大后的他在回望。玩家不是男孩,玩家是造出鱼的人,也就是让他得以回忆的人。

叙事结构

十句话,按顺序讲,像一篇一次只说一句的短回忆录。每一句都要用六次耐心的点击换来,读者没法跳着看。最后一句之后,鱼仍会随机低语其中某句,就像一段记忆讲完很久以后还在回放。

意象

鱼

藏在身体里的秘密,属于大海、却被带上陆地的东西。

母亲

说“放掉它”的人,也是鱼最后陪着的人。

鱼缸

反转。把海藏进身体的男孩,最后自己待在了玻璃里面。

情绪曲线
  1. 01

    零散

    一开始的点击把碎片留得到处都是,屏幕像一段记不全的童年。

  2. 02

    守住

    玩家学会停在一个地方,第一条鱼出现,故事开始了。

  3. 03

    反转

    最后几句来了:鱼缸里的男孩,陪着母亲的鱼。屏幕上的碎片此时读起来,像是他失去的一切。

机制即意义
动作含义
回到同一个位置→回到一段记忆
六次点击→记忆需要时间成形
留下的碎片→没有成形的尝试
按顺序出现的句子→从头讲起的童年
(03)

结果与迭代

得到了什么

耐心变得看得见:屏幕上留满了玩家每次走神留下的碎片,完整的鱼只有几条。由此得到的感受是“回忆的缓慢”。

速写里的句子
小时候,我在海边抓到了一条鱼。When I was a child, I caught a fish by the sea.
母亲很害怕,叫我放掉它。My mother was frightened and told me to let it go.
我偷偷地把鱼藏在肚子里。I secretly hid the fish in my belly.
后来,它学会了呼吸,而我,渐渐无法适应在陆地的生活。Later, it learned to breathe, while I gradually could not adapt to life on land.
现在,它和母亲坐在一起。Now, it sits with my mother.
而我,在鱼缸里寂寞地翻了个身。And I turned over in the fish tank, all alone.
代码里的决定

六次点击

少到能完成,多到让人感觉是在“守着”。

30 px 的圆

六次点击都要落在第一次点击 30 px 以内:接近即可,不必精确,手能做到。

先按顺序,后随机

十句按顺序出现;读完最后一句后,鱼会随机重复某一句,像记忆在回放。

(04)

代码解读

结构
story[]
十组【中文,英文】句子,用 storyIndex 依次读取。
Blob
柔软的、由噪声塑形的背景形状,慢慢漂移。
FishPart
没能拼成鱼的那次点击留下的碎片。
CompleteFish
完整的鱼,isClicked() 用来触发故事。
mousePressed()
先检查是否点中了鱼,再记录最近六次点击,判断它们是否落在同一处。
mousePressed():六次点击成一条鱼p5.js
function mousePressed() {
  // clicking a finished fish tells the next line of the story
  for (let f of completeFishes) {
    if (f.isClicked(mouseX, mouseY)) {
      let [cn, en] = storyIndex < story.length
        ? story[storyIndex++]
        : random(story);
      currentText = cn + "\n" + en;
      return;
    }
  }

  // remember the last six clicks
  clickHistory.push(createVector(mouseX, mouseY));
  if (clickHistory.length > 6) clickHistory.shift();

  // six clicks within 30 px of each other become a fish
  if (clickHistory.length === 6) {
    let consistent = clickHistory.every(v =>
      dist(v.x, v.y, clickHistory[0].x, clickHistory[0].y) < 30);
    if (consistent) {
      completeFishes.push(new CompleteFish(mouseX, mouseY));
      clickHistory = [];
      return;
    }
  }

  // otherwise, only a fragment is left behind
  fishParts.push(new FishPart(mouseX, mouseY));
}

为方便阅读略作精简。整个机制就是一段只记六次的“记忆”:玩家刚才点在了哪里。

在 OpenProcessing 查看完整源码 ↗上方嵌入的速写里也有 Code 标签,可以查看完整源码。

(05)

方法

这个速写对方法的启发
01

让等待被看见

碎片会留在屏幕上,花掉的时间不会白费。

02

宽容手的误差

30 px 的容差要的是意图,不是精确。

03

故事慢慢给

一条鱼一句话,让叙事和付出绑在一起。

查看整个系列的方法 →