Twine/Harlowe 网页对话式作品开发技术手册
基于项目实战总结的完整开发指南
📚 目录
- 项目基础配置
- 音频系统 (HAL)
- 视频背景
- 字体系统
- 鼠标交互
- 打字机效果
- 变量与数据管理
- 分支与随机系统
- 定时器与延时显示
- 用户输入系统
- CSS样式与动画
- 图片显示技巧
- 标签系统与场景切换
- 对话框系统
- 条件逻辑与结局判定
- 高级技巧
1. 项目基础配置
1.1 StoryData 配置
:: StoryData
{"ifid": "唯一标识符","format": "Harlowe","format-version": "3.3.9","start": "起始段落名","tag-colors": {"office": "red","lab": "orange"},"zoom": 1
}
关键参数说明:
ifid: 故事的唯一标识符start: 游戏开始的第一个段落tag-colors: 为不同标签设置颜色,便于编辑器中识别zoom: 编辑器缩放级别
2. 音频系统 (HAL)
2.1 音频文件定义
在 hal.tracks 特殊段落中定义:
:: hal.tracks
backgroundMusic: audio/background.mp3
hoverSound: audio/hover.mp3
clickSound: audio/click.mp3
goodEndingMusic: audio/good_ending.mp3
badEndingMusic: audio/bad_ending.mp3
支持格式: MP3, OGG, WAV, M4A, AAC, WEBM
2.2 音频播放控制
<!-- 循环播放背景音乐 -->
(track: "backgroundMusic", "loop", true)
(track: "backgroundMusic", "play")<!-- 停止音频 -->
(track: "backgroundMusic", "stop")<!-- 暂停音频 -->
(track: "backgroundMusic", "pause")<!-- 音频音量控制 (0-1) -->
(track: "backgroundMusic", "volume", 0.5)<!-- 淡入效果 (秒数) -->
(track: "backgroundMusic", "fadeIn", 2)<!-- 淡出效果 (秒数) -->
(track: "backgroundMusic", "fadeOut", 2)<!-- 淡入到指定音量 (秒数, 音量) -->
(track: "backgroundMusic", "fadeTo", 2, 0.5)
2.3 鼠标悬停/点击音效
在 StoryJavaScript 段落中添加:
:: StoryJavaScript [script]
/* 鼠标悬停音效 */
$(document).on('mouseenter', 'tw-link', function() {if(window.A) {window.A.track('hoverSound').play();}
});/* 鼠标点击音效 */
$(document).on('mousedown', 'tw-link', function() {if(window.A) {window.A.track('clickSound').play();}
});
2.4 HAL 完整集成代码
HAL (Harlowe Audio Library) 是一个强大的音频库,需要在 StoryScript 段落中引入完整代码。
核心功能:
- ✅ 音频预加载
- ✅ 音量控制面板
- ✅ 静音功能
- ✅ 播放列表支持
- ✅ 音频分组管理
- ✅ 用户偏好保存 (localStorage)
3. 视频背景
3.1 HTML视频元素
在段落中添加:
<video autoplay muted loop id="background-video"><source src="video/background.mp4" type="video/mp4"><source src="video/background.webm" type="video/webm">您的浏览器不支持视频播放。
</video>
属性说明:
autoplay: 自动播放muted: 静音(必须,否则浏览器可能阻止自动播放)loop: 循环播放
3.2 CSS样式配置
:: StoryStylesheet [stylesheet]
#background-video {position: fixed;right: 0;bottom: 0;min-width: 100%; min-height: 100%;width: auto;height: auto;z-index: -100;background-size: cover;object-fit: cover; /* 确保视频填充整个容器 */
}
3.3 确保文字可读性
tw-passage {background-color: rgba(0, 0, 0, 0.5); /* 半透明背景 */padding: 2em;border-radius: 10px;
}
4. 字体系统
4.1 本地字体
@font-face {font-family: 'MyCustomFont';src: url('fonts/IBMPlexMono-Regular.ttf');font-weight: normal;font-style: normal;
}tw-story {font-family: 'MyCustomFont', monospace;
}
4.2 在线字体 (Google Fonts)
@import url('https://fonts.googleapis.com/css2?family=ZCOOL+XiaoWei&display=swap');.text {font-family: 'ZCOOL XiaoWei', serif;
}
4.3 字体变体
/* 粗体 */
font-family: 'IBMPlexMono-Bold', monospace;/* 斜体 */
font-family: 'IBMPlexMono-Italic', monospace;/* 细体 */
font-family: 'IBMPlexMono-Light', monospace;
5. 鼠标交互
5.1 自定义光标
html, body {cursor: url('images/cursor.png'), auto;
}/* 链接悬停时的光标 */
tw-link:hover {cursor: url('images/cursor-hover.png'), pointer;
}
光标图片要求:
- 推荐格式:PNG(支持透明)
- 推荐尺寸:32x32 像素
- 热点位置:(0,0) 左上角
5.2 链接悬停效果
tw-link, .enchantment-link {color: #aaeaff;transition: color 0.3s, text-shadow 0.3s;
}tw-link:hover, .enchantment-link:hover {color: #ffffff;text-shadow: 0 0 8px #ffffff;
}
5.3 自定义悬停样式
.clickText:hover {color: pink;cursor: pointer;
}
6. 打字机效果
6.1 Harlowe原生实现(推荐)
(set: $t1 to "要显示的文本内容"){(set: $p1 to 1) |o1>[](live: 30ms)[(append: ?o1)[(print: $t1's $p1)](set: $p1 to it + 1)(if: $p1 is $t1's length + 1)[(stop:)]]
}
参数说明:
30ms: 每个字符显示间隔(数值越小速度越快)$t1: 存储文本的变量$p1: 当前字符位置
6.2 多段文本打字机
(set: $t1 to "第一段文本")
(set: $t2 to "第二段文本")(live:1s)[{(set: $p1 to 1) |o1>[](live: 30ms)[(append: ?o1)[(print: $t1's $p1)](set: $p1 to it + 1)(if: $p1 is $t1's length + 1)[(stop:)]](stop:)}](live:3s)[{(set: $p2 to 1) |o2>[](live: 30ms)[(append: ?o2)[(print: $t2's $p2)](set: $p2 to it + 1)(if: $p2 is $t2's length + 1)[(stop:)]](stop:)}](live:5s)[[[继续|下一段落]]]
6.3 JavaScript实现(setup函数)
:: StoryJavaScript [script]
setup.typewriter = function(text, targetId, speed) {let i = 0;const elem = $(targetId);const timer = setInterval(function() {if (i < text.length) {elem.append(text.charAt(i));i++;} else {clearInterval(timer);}}, speed);
};
调用方式:
<div id="decision-text"></div>
<script>setup.typewriter("你的文本内容", "#decision-text", 40);
</script>
7. 变量与数据管理
7.1 基础变量
<!-- 设置变量 -->
(set: $playerName to "玩家")
(set: $confidence to 100)
(set: $money to 1000000)<!-- 修改变量 -->
(set: $confidence to $confidence - 10)
(set: $money to $money + 5000)<!-- 布尔值 -->
(set: $hasKey to true)
(set: $isDead to false)
7.2 数组变量
<!-- 创建数组 -->
(set: $items to (a: "Item1", "Item2", "Item3"))<!-- 添加元素 -->
(set: $items to $items + (a: "NewItem"))<!-- 移除元素 -->
(set: $items to $items - (a: "Item1"))<!-- 检查是否包含 -->
(if: (array: ...$items) contains "Item1")[包含该物品
]<!-- 遍历数组 -->
(for: each _item, ...$items)[<span>(print: _item)</span>
]<!-- 数组长度 -->
(print: $items's length)
7.3 临时变量
<!-- 使用下划线开头 -->
(set: _tempValue to 100)
(for: each _item, ...$items)[<!-- _item 是临时变量,只在循环内有效 -->
]
7.4 复杂对象
<!-- 使用多个变量模拟对象 -->
(set: $player_name to "张三")
(set: $player_health to 100)
(set: $player_level to 5)
8. 分支与随机系统
8.1 随机选择 - either
<!-- 随机选择一个选项 -->
(set: $randomChoice to (either: "选项1", "选项2", "选项3"))<!-- 随机跳转 -->
(go-to: (either: "段落1", "段落2", "段落3"))
8.2 洗牌选择 - shuffled
<!-- 从洗牌后的数组中取第一个 -->
(set: $nextEvent to (shuffled: ...$events)'s 1st)<!-- 从数组中移除已选项 -->
(set: $events to $events - (a: $nextEvent))
8.3 数组重置
<!-- 当数组为空时重置 -->
(if: $events's length is 0)[(set: $events to (a: "事件1", "事件2", "事件3"))
]
8.4 随机数生成
<!-- 生成1-100的随机数 -->
(set: $randomNum to (random: 1, 100))<!-- 基于概率的分支 -->
(if: (random: 1, 4) is 1)[<!-- 25%概率触发 -->[[特殊事件]]
](else:)[[[普通事件]]
]
8.5 倒计时随机选择
(set: $counter to 15)
You have |amount>[$counter] seconds left!(live: 1s)[(set: $counter to it - 1)(if: $counter is 0)[(go-to: (either: "选择A", "选择B"))](replace: ?amount)[$counter]
]
9. 定时器与延时显示
9.1 延时显示内容
<!-- 1秒后显示 -->
(live: 1s)[(stop:)显示的内容
]<!-- 3秒后显示链接 -->
(live: 3s)[(stop:)[[继续游戏|下一关]]
]
9.2 多段延时显示
(live: 1s)[第一段文字(stop:)]
(live: 3s)[第二段文字(stop:)]
(live: 5s)[[[继续|下一段落]](stop:)]
9.3 倒计时系统
(set: $counter to 30)
<p>You have (css: "color: red;")[|amount>[$counter]] seconds left!</p>(live: 1s)[(set: $counter to it - 1)(if: $counter is 0)[(go-to: "超时段落")](replace: ?amount)[$counter]
]
9.4 停止其他timer
<!-- 在某个live块中停止自己 -->
(live: 5s)[内容显示(stop:) <!-- 这会停止这个live块 -->
]
10. 用户输入系统
10.1 文本输入
请输入你的名字:
(input: bind $playerName, "默认值")[[确认|下一段落]]
实际应用:
:: 输入名字
请输入你的名字:
(input: bind $tempName, "")(link: "确认")[(if: $tempName is not "")[(set: $playerName to $tempName)(go-to: "游戏开始")](else:)[(replace: ?error)[请输入名字!]]
]|error>[]
10.2 单选按钮
<form><label><input type="radio" name="gender" value="male" checked> 男性</label><label><input type="radio" name="gender" value="female"> 女性</label>
</form>
10.3 循环选择器
:: 循环选择
(set: $choices to (array: "选项1", "选项2", "选项3"))当前选择: [(display: "Cycling")]<choice|:: Cycling
(link-repeat: (text: $choices's 1st))[(set: $choices to (rotated: -1, ...$choices))(replace: ?choice)[(display: "Cycling")]
]
工作原理:
(rotated: -1, ...): 向左旋转数组link-repeat: 可重复点击的链接(replace: ?choice): 替换指定标记的内容
11. CSS样式与动画
11.1 淡入动画
.fade-in {animation: fadeInAnimation 4s ease-in forwards;opacity: 0;
}@keyframes fadeInAnimation {from { opacity: 0; }to { opacity: 1; }
}
11.2 抖动效果
.shake {animation: shake 0.5s infinite;
}@keyframes shake {0%, 100% { transform: translateX(0); }25% { transform: translateX(-10px); }75% { transform: translateX(10px); }
}
11.3 故障效果 (Glitch)
.glitch {animation: glitch 3s infinite;
}@keyframes glitch {0% {text-shadow: 0.05em 0 0 #ff0000, -0.05em -0.025em 0 #00ffff;}14% {text-shadow: 0.05em 0 0 #ff0000, -0.05em -0.025em 0 #00ffff;}15% {text-shadow: -0.05em -0.025em 0 #ff0000, 0.025em 0.025em 0 #00ffff;}/* ... 更多帧 */
}
11.4 模糊效果 (用于神经错乱等效果)
tw-story[tags~="neuroepisode"] {animation: neuroepisode 5s ease;
}@keyframes neuroepisode {0% { filter: blur(0); }25% { filter: blur(5px); }50% { filter: blur(0); }75% { filter: blur(3px); }100% { filter: blur(0); }
}
11.5 渐显内容
.hidden-content {opacity: 0;animation: fadeIn 1s forwards;animation-delay: 2s;
}@keyframes fadeIn {to { opacity: 1; }
}
12. 图片显示技巧
12.1 基础图片
<img src="http://example.com/image.jpg" />
12.2 带CSS类的图片
<img class="centered" src="图片URL" />
<img class="draw" src="图片URL" />
<img class="header-image centered70" src="图片URL" />
CSS定义:
.centered {display: block;margin: 0 auto;
}.draw {display: block;width: 600px;height: auto;margin-left: auto;margin-right: auto;
}.header-image {width: 100%;max-width: 800px;
}.centered70 {width: 70%;margin: 0 auto;
}
12.3 图片定位
<img src="URL" style="float:left; margin-right:12px; width:300px; height:1200;">
13. 标签系统与场景切换
13.1 为段落添加标签
:: 段落名 [office lab] {"position":"100,100","size":"100,100"}
段落内容
13.2 基于标签切换背景
tw-story[tags~="office"] {background-image: url("images/office-bg.jpg");background-size: cover;
}tw-story[tags~="lab"] {background-image: url("images/lab-bg.jpg");background-size: cover;
}tw-story[tags~="forest"] {background-image: url("images/forest-bg.jpg");background-size: cover;
}
优势:
- 自动切换背景
- 无需在每个段落中写代码
- 便于管理大量场景
14. 对话框系统
14.1 基础对话框
<div class="dialog"><span class="character-name">角色名:</span> <span class="typewriter">对话内容</span>
</div>
14.2 对话框样式
.dialog {background-color: rgba(0, 0, 0, 0.7);padding: 1em;margin: 0.5em 0;border-radius: 5px;
}.character-name {color: #4ed6ff;font-weight: bold;text-shadow: 0 0 5px #4ed6ff;
}.typewriter {color: #ffffff;
}
14.3 不同角色的对话样式
.chenduo {color: #ff9999;
}.linxia {color: #99ff99;
}.player {color: #9999ff;
}
使用示例:
<p class="chenduo"><span class="emotion">😊</span> 陈岩说:"这是个好主意!"
</p>
14.4 内心独白
.inner-thought {font-style: italic;color: #a0e0ff;border-left: 3px solid #48baec;padding-left: 1em;
}
15. 条件逻辑与结局判定
15.1 基础条件
(if: $confidence >= 70)[高信赖度内容
](else-if: $confidence >= 40)[中等信赖度内容
](else:)[低信赖度内容
]
15.2 多条件判断
(if: $AIAlignmentIndex >= 100 and $Money > 0)[(go-to: "完美结局")
](else-if: $AIAlignmentIndex >= 100 and $Money < 0)[(go-to: "成功但破产结局")
](else-if: $confidence <= 0)[(go-to: "失败结局")
]
15.3 物品检查
(if: (array: ...$items) contains "钥匙")[你有钥匙,可以开门
](else:)[门被锁住了
]
15.4 历史记录检查
<!-- 检查是否访问过某个段落 -->
(if: (history:) contains "特定段落")[你之前来过这里
]<!-- 检查上一个段落 -->
(if: (history:)'s last is "上个段落")[你是从那里来的
]
15.5 自动结局判定
<!-- 在每个段落结束时检查 -->
(if: $health <= 0)[(go-to: "死亡结局")
](else-if: $score >= 100)[(go-to: "胜利结局")
](else:)[[[继续游戏|下一关]]
]
16. 高级技巧
16.1 进度条显示
HTML结构:
<div class="intimacy-container"><span class="label">亲密度:</span><div class="progress-background"><div class="progress-bar" style="width: 30%;"></div></div><span class="percentage">30%</span>
</div>
CSS样式:
.progress-background {width: 100%;height: 20px;background-color: rgba(255, 255, 255, 0.2);border-radius: 10px;overflow: hidden;
}.progress-bar {height: 100%;background: linear-gradient(90deg, #ff6b9d 0%, #c06c84 100%);transition: width 0.5s ease;
}
16.2 状态栏显示
<div class="stats-bar">开心度: 8/10 | 精力: 10/10 | 羁绊: 5/10
</div>
CSS定位:
.stats-bar {position: absolute;top: -40px;left: 20px;color: white;font-family: 'ZCOOL XiaoWei', serif;font-size: 12px;
}
16.3 动态变量显示
(text-colour:black)+(bg:white)[计算积分 - (text-style:"bold")[$Money] 点声誉 - (text-style:"bold")[$Reputation%]压力水平 - (text-colour:red)+(text-style:"bold")[$PersonalHealth%]
]
16.4 链接样式修饰
<!-- 带对话框提示的链接 -->
(link-repeat: "考虑选项A")[(dialog: "思考分析", "这是详细分析内容")(goto: "选项A段落")
]
16.5 自动更新显示
<!-- 使用命名钩子自动更新 -->
当前金钱: |money>[$money]<!-- 在其他地方更新 -->
(set: $money to $money + 100)
(replace: ?money)[$money]
16.6 隐藏/显示侧边栏
<tw-sidebar><tw-icon tabindex="0" class="undo" title="Undo" style="visibility: hidden;">↶</tw-icon><tw-icon tabindex="0" class="redo" title="Redo" style="visibility: hidden;">↷</tw-icon>
</tw-sidebar>
16.7 对话框 (dialog)
(link-repeat: "查看提示")[(dialog: "提示标题", "这是提示内容\n可以多行显示")
]
16.8 Link样式控制
<!-- 改变链接颜色 -->
[[<span style="color: orange;">南瓜</span>|选择南瓜]]<!-- 带特殊效果的链接 -->
(link-goto: "继续", "下一段落")
(link-reveal-goto: "确认", "目标段落")[(set: $confirmed to true)
]
16.9 过渡效果
(transition:'dissolve')[溶解显示的内容]
(transition:'pulse')[脉冲效果]
(transition:'slide-right')[从右滑入]<!-- 带延迟的过渡 -->
(transition:"pulse")+(transition-delay:2s)+(transition-time:3s)[内容]
17. JavaScript集成技巧
17.1 Setup命名空间
:: StoryJavaScript [script]
setup.updateStatus = function() {// 更新状态显示的逻辑
};setup.typewriter = function(text, target, speed) {// 打字机效果实现
};
17.2 段落事件监听
$(document).on(':passagestart', function (ev) {// 每次进入新段落时执行var confidence = State.variables.confidence;if (confidence <= 0) {Engine.play("Bad Ending");}
});
17.3 音频控制
// 在特定条件下播放音频
$(document).on(':passagerender', function (ev) {if (State.variables.isInBattle) {A.track('battleMusic').play();}
});
18. 数据持久化
18.1 localStorage保存
<!-- HAL自动保存音频偏好 -->
<!-- 需要在hal.config中设置 -->
:: hal.config
persistPrefs: true
18.2 Session存储
HAL会自动保存:
- 音轨列表
- 播放列表
- 音频组
18.3 自定义保存
// 保存数据
if (window.localStorage) {localStorage.setItem('saveData', JSON.stringify(State.variables));
}// 读取数据
if (window.localStorage) {var data = JSON.parse(localStorage.getItem('saveData'));State.variables = data;
}
19. 项目中未涉及但常用的技术
19.1 渐变文字
.gradient-text {background: linear-gradient(45deg, #ff6b9d, #c06c84, #6c5b7b);-webkit-background-clip: text;-webkit-text-fill-color: transparent;background-clip: text;
}
19.2 打字光标效果
.typing-cursor::after {content: '|';animation: blink 1s infinite;
}@keyframes blink {0%, 49% { opacity: 1; }50%, 100% { opacity: 0; }
}
19.3 粒子背景效果
需要引入 particles.js 库:
<div id="particles-js"></div><script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
<script>particlesJS('particles-js', {particles: {number: { value: 80 },color: { value: '#ffffff' },shape: { type: 'circle' }}});
</script>
19.4 全屏按钮
function toggleFullscreen() {if (!document.fullscreenElement) {document.documentElement.requestFullscreen();} else {document.exitFullscreen();}
}
19.5 保存/加载系统
:: 保存游戏
(set: $saveData to (dm: "money", $money,"reputation", $reputation,"currentPassage", (passage:)'s name
))(if: (save-game: "存档1", $saveData))[游戏已保存!
](else:)[保存失败!
]:: 加载游戏
(if: (saved-games:) contains "存档1")[(load-game: "存档1")
](else:)[没有找到存档
]
19.6 章节跳过功能
:: 章节选择
(link: "跳到第二章")[(set: $chapter to 2)(go-to: "第二章开始")]
(link: "跳到第三章")[(set: $chapter to 3)(go-to: "第三章开始")]
19.7 成就系统
:: 检查成就
(set: $achievements to (a:))(if: $money >= 1000000)[(set: $achievements to $achievements + (a: "百万富翁"))
](if: $reputation >= 90)[(set: $achievements to $achievements + (a: "声名远扬"))
]<!-- 显示成就 -->
已解锁成就:
(for: each _ach, ...$achievements)[🏆 (print: _ach)
]
19.8 背景音乐混合与切换
<!-- 淡出旧音乐,淡入新音乐 -->
(track: "oldMusic", "fadeOut", 2)
(live: 2s)[(stop:)(track: "newMusic", "fadeIn", 2)
]
19.9 屏幕震动效果
@keyframes shake-screen {0%, 100% { transform: translate(0, 0); }10%, 30%, 50%, 70%, 90% { transform: translate(-10px, 0); }20%, 40%, 60%, 80% { transform: translate(10px, 0); }
}.shake-screen {animation: shake-screen 0.5s;
}
// 触发震动
$('tw-story').addClass('shake-screen');
setTimeout(function() {$('tw-story').removeClass('shake-screen');
}, 500);
19.10 文字打字音效
:: 打字音效打字机
(set: $text to "要显示的文本")
{(set: $pos to 1) |output>[](live: 50ms)[(append: ?output)[(print: $text's $pos)](track: "typeSound", "play") <!-- 每个字符播放音效 -->(set: $pos to it + 1)(if: $pos is $text's length + 1)[(stop:)]]
}
20. 性能优化建议
20.1 音频预加载
:: hal.config
preload: true
loadDelay: 0
trackLoadLimit: 500
totalLoadLimit: 8000
20.2 延迟加载图片
<img src="placeholder.jpg" data-src="actual-image.jpg" class="lazy-load"><script>// 使用Intersection Observer实现懒加载const images = document.querySelectorAll('.lazy-load');const observer = new IntersectionObserver((entries) => {entries.forEach(entry => {if (entry.isIntersecting) {entry.target.src = entry.target.dataset.src;observer.unobserve(entry.target);}});});images.forEach(img => observer.observe(img));
</script>
20.3 减少live宏使用
<!-- 不推荐:多个独立的live -->
(live: 1s)[文本1(stop:)]
(live: 2s)[文本2(stop:)]
(live: 3s)[文本3(stop:)]<!-- 推荐:组合使用 -->
(live: 1s)[文本1(live: 1s)[文本2(live: 1s)[文本3(stop:)](stop:)](stop:)
]
21. 调试技巧
21.1 控制台输出
<!-- 变量调试 -->
(print: $myVariable)<!-- 在浏览器控制台输出 -->
<script>console.log(State.variables);</script>
21.2 HAL调试模式
:: hal.config
debug: true
21.3 显示当前段落名
当前段落: (print: (passage:)'s name)
22. 常见问题与解决方案
22.1 音频无法自动播放
原因: 浏览器政策限制
解决方案:
<!-- 使用playWhenPossible -->
(track: "bgMusic", "playWhenPossible")<!-- 或在用户交互后播放 -->
[[开始游戏|游戏开始]]
<!-- 在"游戏开始"段落中播放音频 -->
22.2 视频在移动设备上不循环
解决方案:
<video autoplay muted loop playsinline webkit-playsinline><source src="video.mp4" type="video/mp4">
</video>
22.3 中文字符显示问题
<head><meta charset="UTF-8">
</head>
22.4 变量未定义错误
<!-- 初始化所有变量 -->
:: 初始化 [startup]
(set: $money to 0)
(set: $items to (a:))
(set: $flags to (dm:))
23. 完整模板示例
23.1 基础游戏模板
:: StoryTitle
我的Twine游戏:: StoryData
{"ifid": "UNIQUE-ID-HERE","format": "Harlowe","format-version": "3.3.9","start": "开始","zoom": 1
}:: hal.tracks
bgMusic: audio/bg.mp3
clickSound: audio/click.mp3
hoverSound: audio/hover.mp3:: StoryStylesheet [stylesheet]
@font-face {font-family: 'CustomFont';src: url('fonts/font.ttf');
}body {cursor: url('images/cursor.png'), auto;background: #000;
}tw-story {font-family: 'CustomFont', sans-serif;color: #fff;
}tw-link:hover {color: #ff69b4;text-shadow: 0 0 10px #ff69b4;
}:: StoryJavaScript [script]
// 鼠标音效
$(document).on('mouseenter', 'tw-link', function() {if(window.A) A.track('hoverSound').play();
});$(document).on('mousedown', 'tw-link', function() {if(window.A) A.track('clickSound').play();
});// 打字机函数
setup.typewriter = function(text, target, speed) {let i = 0;const elem = $(target);const timer = setInterval(function() {if (i < text.length) {elem.append(text.charAt(i));i++;} else {clearInterval(timer);}}, speed);
};:: 开始
(set: $playerName to "")
(set: $health to 100)
(set: $items to (a:))(track: "bgMusic", "loop", true)
(track: "bgMusic", "play")<h1>游戏标题</h1>请输入你的名字:
(input: bind $playerName, "冒险者")[[开始冒险|第一章]]:: 第一章
(set: $text to "欢迎来到冒险世界," + $playerName + "!"){(set: $pos to 1)|output>[](live: 50ms)[(append: ?output)[(print: $text's $pos)](set: $pos to it + 1)(if: $pos is $text's length + 1)[(stop:)]]
}(live: 3s)[(stop:)[[向北走|森林]][[向南走|城镇]]
]
24. 最佳实践
24.1 文件组织
项目目录/
├── story.twee # 主故事文件
├── audio/ # 音频文件
│ ├── bg.mp3
│ ├── click.mp3
│ └── hover.mp3
├── video/ # 视频文件
│ └── background.mp4
├── fonts/ # 字体文件
│ └── custom.ttf
└── images/ # 图片文件├── cursor.png└── backgrounds/
24.2 命名规范
- 段落名: 使用清晰描述性名称,如
第一章_森林入口 - 变量名: 使用驼峰命名,如
$playerName,$currentHealth - 临时变量: 使用下划线开头,如
_tempValue - 数组: 使用复数形式,如
$items,$achievements
24.3 代码可读性
<!-- 推荐:清晰的缩进和注释 -->
(if: $health > 0)[<!-- 玩家存活 -->(if: $hasWeapon)[你可以战斗](else:)[你需要寻找武器]
](else:)[<!-- 玩家死亡 -->(go-to: "游戏结束")
]
24.4 性能优化
- 避免过度使用live宏
- 合理使用图片压缩
- 音频文件使用适当比特率
- 减少不必要的CSS动画
25. 资源引用
25.1 在线资源CDN
<!-- Google Fonts -->
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap');<!-- jQuery (Harlowe已包含) -->
<!-- Particles.js -->
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
25.2 本地资源
/* 相对路径 */
background-image: url('images/bg.jpg');
src: url('fonts/font.ttf');/* 绝对路径 */
background-image: url('/assets/images/bg.jpg');
26. 响应式设计
26.1 媒体查询
/* 桌面端 */
@media screen and (min-width: 1024px) {tw-passage {width: 60%;font-size: 1.2em;}
}/* 平板 */
@media screen and (max-width: 1023px) and (min-width: 768px) {tw-passage {width: 80%;font-size: 1em;}
}/* 移动端 */
@media screen and (max-width: 767px) {tw-passage {width: 95%;font-size: 0.9em;}.stats-bar {font-size: 10px;}
}
27. 特殊效果实现
27.1 文字逐渐显现(reveal)
(link-reveal: "点击查看隐藏内容")[这是隐藏的内容
]
27.2 替换文本
原始文本 (link-replace: "点击替换")[替换后的文本]
27.3 追加文本
|hook>[](link: "添加内容")[(append: ?hook)[新增的内容]
]
27.4 倒计时加载屏幕
(set: $countdown to 5)
Loading... |time>[$countdown](live: 1s)[(set: $countdown to it - 1)(replace: ?time)[$countdown](if: $countdown is 0)[(stop:)(go-to: "游戏主菜单")]
]
28. 错误处理
28.1 音频加载失败
$(document).on(':available', function(ev) {console.log('音频可用:', ev.track.id);
});$(document).on(':loaded', function(ev) {console.log('音频加载完成:', ev.track.id);
});
28.2 变量检查
(if: $myVar is not undefined)[变量已定义
](else:)[变量未定义,使用默认值(set: $myVarto 0)
]
29. 高级音频技术
29.1 播放列表
<!-- 创建播放列表 -->
(newplaylist: "myPlaylist", "track1", "track2", "track3")<!-- 播放播放列表 -->
(playlist: "myPlaylist", "play")<!-- 洗牌播放 -->
(playlist: "myPlaylist", "shuffle")
(playlist: "myPlaylist", "play")<!-- 停止播放列表 -->
(playlist: "myPlaylist", "stop")
29.2 音频分组
<!-- 创建音频组 -->
(newgroup: "sfxGroup", "click", "hover", "whoosh")<!-- 控制整组音频 -->
(group: "sfxGroup", "volume", 0.3)
(group: "sfxGroup", "mute", true)
29.3 音频事件监听
// 监听音频播放事件
A.on('play', function(ev) {console.log('正在播放:', ev.track.id);
});// 监听音频停止事件
A.on('stop', function(ev) {console.log('已停止:', ev.track.id);
});
30. 实战技巧汇总
30.1 状态更新系统
:: StoryJavaScript [script]
setup.updateStatus = function() {// 更新所有状态显示$('#money-display').text('$' + State.variables.money);$('#reputation-display').text(State.variables.reputation + '%');
};
<!-- 在段落中调用 -->
(set: $money to $money + 1000)
<script>setup.updateStatus();</script>
30.2 对话框预设样式
<!-- 使用一致的对话框结构 -->
<div class="textbox" style="width: 720px;"><img src="对话框背景.jpg" style="width: 100%; height: auto;"><div class="stats-bar">开心度: 8/10 | 精力: 10/10</div><div class="text">对话文本内容</div>
</div>
30.3 场景描述样式
<div class="scene-description">环境描述文字
</div><div class="scene-transition">场景转换提示
</div>
30.4 结局提示样式
<div class="outcome"><p><span class="emotion">✓</span> 成功提示</p>
</div><div class="outcome fail"><p><span class="emotion">❌</span> 失败提示</p>
</div>
31. 特效代码片段库
31.1 文字颜色动态变化
.color-shift {animation: colorShift 3s infinite;
}@keyframes colorShift {0% { color: #ff6b9d; }33% { color: #c06c84; }66% { color: #6c5b7b; }100% { color: #ff6b9d; }
}
31.2 脉冲发光效果
.pulse-glow {animation: pulseGlow 2s ease-in-out infinite;
}@keyframes pulseGlow {0%, 100% {text-shadow: 0 0 5px #4ed6ff;}50% {text-shadow: 0 0 20px #4ed6ff, 0 0 30px #4ed6ff;}
}
31.3 悬浮动画
.float {animation: float 3s ease-in-out infinite;
}@keyframes float {0%, 100% { transform: translateY(0px); }50% { transform: translateY(-20px); }
}
31.4 旋转加载动画
.spinner {display: inline-block;width: 40px;height: 40px;border: 4px solid rgba(255,255,255,0.3);border-radius: 50%;border-top-color: #fff;animation: spin 1s linear infinite;
}@keyframes spin {to { transform: rotate(360deg); }
}
32. 项目文件结构建议
MyTwineProject/
├── story.twee # 主故事文件
├── 素材/
│ ├── 音乐/
│ │ ├── BGM.mp3 # 背景音乐
│ │ ├── 点击.MP3 # 点击音效
│ │ └── 光标掠过选项.MP3 # 悬停音效
│ ├── 视频/
│ │ └── Background.mp4 # 背景视频
│ ├── 字体/
│ │ └── Custom.ttf # 自定义字体
│ └── 图片/
│ ├── 光标.png # 自定义光标
│ └── 场景/ # 场景图片
└── README.md # 项目说明
33. 常用Harlowe宏速查
33.1 文本样式
(text-colour: red)[红色文字]
(text-style: "bold")[粗体]
(text-style: "italic")[斜体]
(text-style: "underline")[下划线]
(text-style: "double-underline")[双下划线]
(text-style: "rumble")[震动文字]
(text-size: 1.5)[放大1.5倍]
(css: "font-size: 150%;")[自定义CSS]
33.2 对齐
(align: "=><=")[居中]
(align: "==><==")[完全居中]
(align: "<==")[右对齐]
33.3 背景色
(bg: white)[白色背景]
(bg: (rgb: 255, 0, 0))[RGB背景]
34. 调试命令
34.1 显示所有变量
(print: (datanames:))
(print: (datavalues:))
34.2 查看历史
访问过的段落:(print: (history:))
上一个段落:(print: (history:)'s last)
34.3 强制跳转
<!-- 无条件跳转 -->
(go-to: "目标段落")<!-- 根据变量跳转 -->
(goto: $targetPassage)
35. 移动端优化
35.1 触摸友好设计
/* 增大可点击区域 */
tw-link {padding: 0.5em 1em;display: inline-block;min-height: 44px; /* iOS推荐最小触摸区域 */
}
35.2 禁用文字选择
body {-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;
}
35.3 视口设置
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
36. 高级变量技巧
36.1 DataMap (数据映射)
<!-- 创建数据映射 -->
(set: $player to (dm: "name", "张三","health", 100,"items", (a: "剑", "shield")
))<!-- 访问值 -->
(print: $player's name)
(print: $player's health)<!-- 修改值 -->
(set: $player's health to 80)
36.2 DataSet (数据集)
<!-- 创建集合(无重复元素) -->
(set: $achievements to (ds: "新手", "探索者"))<!-- 添加元素 -->
(set: $achievements to $achievements + (ds: "勇士"))<!-- 检查是否包含 -->
(if: $achievements contains "新手")[你是新手
]
37. 完整示例:对话系统
:: 对话场景
(set: $dialogues to (a:"你好,欢迎来到这里。","我是这里的守护者。","你有什么问题吗?"
))(set: $currentDialog to 0)<div class="dialog-box"><div class="character-portrait"><img src="npc.jpg"></div><div class="character-name">守护者</div><div class="dialog-text">|dialog>[(print: $dialogues's ($currentDialog + 1))]</div>
</div>(if: $currentDialog < $dialogues's length - 1)[(link: "继续 →")[(set: $currentDialog to it + 1)(replace: ?dialog)[(print: $dialogues's ($currentDialog + 1))]]
](else:)[[[对话结束|下一场景]]
]
38. 发布前检查清单
- 所有音频文件路径正确
- 字体文件已包含
- 视频文件大小合理(推荐<50MB)
- 图片已压缩优化
- 在不同浏览器测试(Chrome, Firefox, Safari)
- 移动端测试
- 检查所有链接是否正确
- 变量初始化完整
- 移除调试代码
- 拼写检查
39. 关键性能指标
39.1 推荐限制
- 音频文件: 单个 < 10MB,总计 < 50MB
- 视频文件: < 50MB (考虑压缩)
- 图片文件: 单个 < 500KB
- 字体文件: 单个 < 2MB
- 总项目大小: < 100MB
39.2 加载优化
:: hal.config
preload: true # 预加载音频
loadDelay: 0 # 加载延迟(毫秒)
trackLoadLimit: 500 # 单个音轨加载超时(毫秒)
totalLoadLimit: 8000 # 总加载超时(毫秒)
40. 推荐工具与资源
40.1 开发工具
- Twine 2: https://twinery.org/
- Tweego: 命令行编译工具
- VS Code: 配合twee3语言服务器
40.2 资源网站
- 免费音效: Freesound.org, Pixabay
- 免费音乐: YouTube Audio Library
- 免费字体: Google Fonts, Font Squirrel
- 图片: Unsplash, Pexels
40.3 社区资源
- Twine Wiki: https://twinery.org/wiki/
- Harlowe手册: https://twine2.neocities.org/
- HAL文档: Chapel’s Audio Library
附录A:完整的HAL配置示例
:: hal.config
preload: true
loadDelay: 0
muteOnBlur: true
startingVol: 0.5
persistPrefs: true
globalA: true
showControls: true
sidebarStartClosed: true
volumeDisplay: false
trackLoadLimit: 500
totalLoadLimit: 8000
debug: false
参数说明:
preload: 预加载所有音频muteOnBlur: 窗口失焦时静音startingVol: 初始音量 (0-1)persistPrefs: 保存用户偏好globalA: 创建全局A对象showControls: 显示音量控制面板volumeDisplay: 显示音量数值
附录B:完整的项目模板
详见第23节的完整模板示例。
附录C:快捷键参考
在Twine编辑器中:
Ctrl/Cmd + S: 保存Ctrl/Cmd + F: 查找Ctrl/Cmd + Z: 撤销Ctrl/Cmd + Shift + Z: 重做
📖 总结
本手册基于实际项目开发经验总结,涵盖了Twine/Harlowe开发的所有核心技术:
✅ 音频系统: HAL库的完整应用
✅ 视觉效果: 视频背景、图片、动画
✅ 交互设计: 鼠标、键盘、用户输入
✅ 文本效果: 打字机、渐显、样式
✅ 逻辑控制: 变量、条件、随机、定时器
✅ 性能优化: 资源管理、加载策略
✅ 高级技巧: 数据持久化、事件系统
🎯 快速开发流程
- 规划结构 → 设计故事分支图
- 配置基础 → StoryData + hal.tracks
- 准备资源 → 音频、视频、图片、字体
- 编写样式 → StoryStylesheet
- 实现逻辑 → 变量系统 + 分支逻辑
- 添加特效 → 打字机 + 动画
- 测试优化 → 多浏览器测试
- 发布 → 导出HTML文件
文档版本: v1.0
最后更新: 2025-01-10
基于: 实际项目文件分析
适用: Harlowe 3.3.9+
相关文件参考
本项目中的实际应用示例:
MapIsUnavailable.twee: HAL音频系统完整实现_____5.twee: 打字机效果与状态栏- [
2060_ London Hive Crisis.twee](2060_ London Hive Crisis.twee): 标签系统与场景切换 - [
AI Alignment crisis_ Novaco_.twee](AI Alignment crisis_ Novaco_.twee): 变量管理与条件判断 ____Revelation_.twee: 音频控制与用户输入
提示: 所有代码示例均已在实际项目中验证有效。建议先从简单功能开始,逐步添加复杂特性。
