游戏开发数据驱动设计:告别硬编码,实现逻辑配置化
2026/9/16 8:27:20 网站建设 项目流程

1. 这篇文章真正要解决的问题

如果你是一名游戏开发者,或者正在使用游戏引擎(如Unity、Unreal Engine)进行项目开发,那么你一定遇到过这样的困境:如何高效、自动化地处理游戏内复杂的数值计算、状态管理和战斗逻辑?尤其是在开发MMORPG、ARPG或带有深度养成系统的游戏时,角色的属性成长、技能伤害公式、战斗结算等模块,往往充斥着大量硬编码的if-else语句和散落在各处的魔法数字。一旦策划需求变更,比如调整“升星”、“渡劫”、“重数”带来的属性加成,或者修改“炼化”对手的伤害算法,程序员就需要在代码的海洋里艰难地定位和修改,测试工作量也随之激增。

本文将通过一个高度抽象和简化的案例——解析类似“潇龙飞打把打黑赛赢得的钱渡阶,升43星2阶100重后回去炼化了韩服几个小啰啰”这样的游戏内事件描述——来探讨一个核心问题:如何将游戏业务逻辑从硬编码中解放出来,实现数据驱动与配置化?这个看似无厘头的标题,实际上浓缩了多个游戏开发中的经典模块:经济系统(赢钱)、角色成长(渡阶/升星/升重)、战斗系统(炼化)。我们将以此为契机,深入讲解如何设计一个灵活、可扩展的游戏逻辑框架。

读完本文,你将能清晰地理解:

  1. 数据驱动设计的核心思想及其在游戏开发中的压倒性优势。
  2. 如何构建一个配置化的角色属性与成长系统,让策划能通过Excel或JSON自由调整“星”、“阶”、“重”的数值。
  3. 如何实现一个可脚本化或规则引擎化的战斗系统,让“炼化”(即战斗结算)逻辑可以通过配置动态改变,而无需重新编译代码。
  4. 一套完整的、基于通用编程语言(如Python/TypeScript)的示例架构与代码,你可以直接借鉴到你的Unity C#或Unreal C++项目中。

2. 核心概念:数据驱动与游戏逻辑配置化

在深入代码之前,我们必须统一思想。传统游戏开发模式与数据驱动模式的关键区别在于“逻辑”存放的位置。

传统硬编码模式:游戏规则直接写在程序代码里。例如,升级所需的经验值可能是一个函数:

def get_exp_required(level): if level < 10: return level * 100 elif level < 30: return 900 + (level - 9) * 150 else: return 3900 + (level - 29) * 200

缺点:任何修改(如调整30级后的经验曲线)都需要程序员修改代码、重新编译、打包,测试周期长,极易出错。

数据驱动配置化模式:游戏规则存储在程序之外的数据文件(如JSON, XML, Excel, ScriptableObject)中。程序读取这些配置数据来执行逻辑。

// exp_config.json { "exp_curve": [ {"level_range": [1, 9], "base_exp": 100, "increment": 100}, {"level_range": [10, 29], "base_exp": 900, "increment": 150}, {"level_range": [30, 100], "base_exp": 3900, "increment": 200} ] }

优点

  • 策划友好:策划人员可以在不接触代码的情况下调整数值平衡。
  • 热更新可能:可以远程更新配置文件,实现不停机调整。
  • 维护清晰:所有游戏规则集中管理,一目了然。
  • 降低耦合:业务逻辑与核心引擎分离,系统更健壮。

对应到我们的标题案例:

  • 赢得的钱->经济系统配置(不同比赛奖金表)。
  • 渡阶,升43星2阶100重->角色成长配置(每阶、每星、每重对应的属性加成表)。
  • 炼化了韩服几个小啰啰->战斗系统配置(伤害计算公式、胜负条件、奖励规则)。

3. 环境准备与项目结构

我们将使用Python作为演示语言,因为它语法简洁,能清晰表达设计思想,并且这些思想可以无缝迁移到C#/Java/C++等游戏开发主流语言。所有示例都将注重架构而非语法。

假设环境:

  • Python 3.8+
  • 无需特殊第三方库,使用标准库json

项目结构规划:

game_logic_framework/ ├── configs/ # 存放所有游戏配置 │ ├── economy.json # 经济系统配置(比赛奖金) │ ├── growth.json # 成长系统配置(阶、星、重) │ └── combat.json # 战斗系统配置(伤害公式) ├── models/ # 数据模型类 │ ├── character.py # 角色类 │ └── enemy.py # 敌人类 ├── engines/ # 逻辑引擎 │ ├── growth_engine.py # 成长计算引擎 │ └── combat_engine.py # 战斗计算引擎 ├── utils/ │ └── config_loader.py # 配置加载器 └── main.py # 主程序入口

4. 第一步:构建数据模型与配置加载器

任何系统的起点都是定义清晰的数据结构。

1. 角色模型 (models/character.py)

# models/character.py class Character: def __init__(self, name): self.name = name self.money = 0 # 成长系统属性 self.tier = 0 # 阶 self.star = 0 # 星 self.refinement = 0 # 重 # 战斗属性(由成长系统影响) self.attack = 100 self.defense = 50 self.hp = 1000 def __str__(self): return f"{self.name} [Lv: {self.tier}阶{self.star}星{self.refinement}重] ATK:{self.attack} DEF:{self.defense} HP:{self.hp} Money:{self.money}"

2. 通用配置加载器 (utils/config_loader.py)

# utils/config_loader.py import json import os class ConfigLoader: _configs = {} @classmethod def load_config(cls, config_name): """加载并缓存配置""" if config_name not in cls._configs: config_path = os.path.join('configs', f'{config_name}.json') try: with open(config_path, 'r', encoding='utf-8') as f: cls._configs[config_name] = json.load(f) except FileNotFoundError: print(f"警告:配置文件 {config_path} 未找到,返回空配置。") cls._configs[config_name] = {} except json.JSONDecodeError as e: print(f"错误:配置文件 {config_path} JSON格式错误 - {e}") cls._configs[config_name] = {} return cls._configs[config_name] @classmethod def get_economy_config(cls): return cls.load_config('economy') @classmethod def get_growth_config(cls): return cls.load_config('growth') @classmethod def get_combat_config(cls): return cls.load_config('combat')

5. 第二步:实现经济系统——比赛与奖励

“打黑赛赢得的钱”对应经济系统。我们将其配置化。

1. 经济系统配置 (configs/economy.json)

{ "tournaments": { "black_tournament": { "name": "地下黑赛", "base_reward": 5000, "reward_per_win_streak": 1000, "description": "赢得比赛获得基础奖金,连胜有额外奖励。" }, "arena_weekly": { "name": "每周竞技场", "base_reward": 3000, "reward_per_rank": 500 } } }

2. 经济系统逻辑 (可集成在main.py或独立service中)

# 在 main.py 或 economy_service.py 中 def award_tournament_prize(character, tournament_key, win_streak=0): config = ConfigLoader.get_economy_config() tournament = config.get('tournaments', {}).get(tournament_key) if not tournament: print(f"错误:未找到赛事配置 '{tournament_key}'") return reward = tournament.get('base_reward', 0) # 根据连胜计算额外奖励 reward += tournament.get('reward_per_win_streak', 0) * win_streak character.money += reward print(f"{character.name} 参加了【{tournament.get('name', tournament_key)}】,赢得 {reward} 金币。") print(f" 当前金币:{character.money}")

6. 第三步:实现成长系统——阶、星、重的属性计算

“升43星2阶100重”是成长系统的核心。这是数据驱动最能发挥价值的地方。

1. 成长系统配置 (configs/growth.json)这是最关键的文件,它定义了每一级成长带来的属性变化。

{ "tier_config": { "属性成长类型": "线性叠加", "levels": [ {"tier": 0, "attack_bonus": 0, "defense_bonus": 0, "hp_bonus": 0}, {"tier": 1, "attack_bonus": 50, "defense_bonus": 20, "hp_bonus": 200}, {"tier": 2, "attack_bonus": 120, "defense_bonus": 50, "hp_bonus": 500} // ... 可以配置到很高的阶位 ] }, "star_config": { "属性成长类型": "百分比加成", "levels": [ {"star": 0, "attack_multiplier": 1.0, "defense_multiplier": 1.0, "hp_multiplier": 1.0}, {"star": 1, "attack_multiplier": 1.1, "defense_multiplier": 1.05, "hp_multiplier": 1.08}, {"star": 2, "attack_multiplier": 1.22, "defense_multiplier": 1.12, "hp_multiplier": 1.18} // ... 配置到43星甚至更高 ] }, "refinement_config": { "属性成长类型": "固定值累加", "bonus_per_refinement": { "attack": 3, "defense": 1, "hp": 10 } } }

2. 成长计算引擎 (engines/growth_engine.py)

# engines/growth_engine.py from models.character import Character class GrowthEngine: def __init__(self): self.config = ConfigLoader.get_growth_config() def apply_growth_to_character(self, character: Character, target_tier, target_star, target_refinement): """将角色提升到指定的阶、星、重,并重新计算属性""" old_stats = (character.attack, character.defense, character.hp) # 1. 更新等级 character.tier = target_tier character.star = target_star character.refinement = target_refinement # 2. 计算基础属性(这里假设基础属性为初始值,实际可能受其他系统影响) base_attack = 100 base_defense = 50 base_hp = 1000 # 3. 计算阶位加成(线性叠加) tier_bonus = self._calculate_tier_bonus(target_tier) attack_from_tier = tier_bonus['attack'] defense_from_tier = tier_bonus['defense'] hp_from_tier = tier_bonus['hp'] # 4. 计算星级加成(百分比乘算) star_multiplier = self._calculate_star_multiplier(target_star) # 先加阶位的基础值,再乘星级 attack_after_tier = base_attack + attack_from_tier defense_after_tier = base_defense + defense_from_tier hp_after_tier = base_hp + hp_from_tier attack_after_star = attack_after_tier * star_multiplier['attack'] defense_after_star = defense_after_tier * star_multiplier['defense'] hp_after_star = hp_after_tier * star_multiplier['hp'] # 5. 计算重数加成(固定值累加) refinement_bonus = self._calculate_refinement_bonus(target_refinement) final_attack = attack_after_star + refinement_bonus['attack'] final_defense = defense_after_star + refinement_bonus['defense'] final_hp = hp_after_star + refinement_bonus['hp'] # 6. 赋值(可加入取整逻辑) character.attack = int(final_attack) character.defense = int(final_defense) character.hp = int(final_hp) print(f"{character.name} 成长至 {target_tier}阶{target_star}星{target_refinement}重。") print(f" 属性变化: ATK {old_stats[0]} -> {character.attack}, DEF {old_stats[1]} -> {character.defense}, HP {old_stats[2]} -> {character.hp}") def _calculate_tier_bonus(self, tier): config = self.config.get('tier_config', {}) levels = config.get('levels', []) # 查找小于等于目标阶位的最大配置(支持非连续配置) applicable = [l for l in levels if l['tier'] <= tier] if not applicable: return {'attack': 0, 'defense': 0, 'hp': 0} # 取最高阶的配置(假设配置是递增的) target_level = max(applicable, key=lambda x: x['tier']) return { 'attack': target_level.get('attack_bonus', 0), 'defense': target_level.get('defense_bonus', 0), 'hp': target_level.get('hp_bonus', 0) } def _calculate_star_multiplier(self, star): config = self.config.get('star_config', {}) levels = config.get('levels', []) applicable = [l for l in levels if l['star'] <= star] if not applicable: return {'attack': 1.0, 'defense': 1.0, 'hp': 1.0} target_level = max(applicable, key=lambda x: x['star']) return { 'attack': target_level.get('attack_multiplier', 1.0), 'defense': target_level.get('defense_multiplier', 1.0), 'hp': target_level.get('hp_multiplier', 1.0) } def _calculate_refinement_bonus(self, refinement): config = self.config.get('refinement_config', {}) bonus_per = config.get('bonus_per_refinement', {}) return { 'attack': bonus_per.get('attack', 0) * refinement, 'defense': bonus_per.get('defense', 0) * refinement, 'hp': bonus_per.get('hp', 0) * refinement }

7. 第四步:实现战斗系统——可配置的“炼化”逻辑

“炼化了韩服几个小啰啰”对应战斗。我们将战斗公式和结果判定也配置化。

1. 战斗系统配置 (configs/combat.json)

{ "damage_formula": "attack * (1 + 0.1 * star) - enemy_defense", "damage_formula_v2": "max(0, (attack * (1 + star * 0.05) - enemy_defense * 0.7))", "victory_condition": "character_hp > 0 && enemy_hp <= 0", "loot_table": { "common_enemy": [ {"item": "gold", "amount": [50, 150], "probability": 1.0}, {"item": "potion_small", "amount": [1, 1], "probability": 0.3} ], "elite_enemy": [ {"item": "gold", "amount": [300, 600], "probability": 1.0}, {"item": "equipment_scroll", "amount": [1, 1], "probability": 0.5} ] } }

2. 战斗计算引擎 (engines/combat_engine.py)这里我们使用Python的eval来解析配置中的公式字符串(生产环境请使用更安全的表达式解析库,如asteval或自定义解析器)。

# engines/combat_engine.py import random from models.character import Character class CombatEngine: def __init__(self): self.config = ConfigLoader.get_combat_config() def resolve_combat(self, character: Character, enemy_type, enemy_count=1): """解决一场战斗""" print(f"\n{character.name} 遭遇了 {enemy_count} 个【{enemy_type}】。") total_loot = {'gold': 0} for i in range(enemy_count): print(f"\n > 战斗回合 {i+1}:") # 1. 模拟敌人属性(这里简化,可从配置读取) enemy_attack = 80 enemy_defense = 30 enemy_hp = 400 # 2. 应用伤害公式(从配置读取) damage_formula = self.config.get('damage_formula_v2', 'attack - enemy_defense') # 为公式提供上下文变量 context = { 'attack': character.attack, 'defense': character.defense, 'hp': character.hp, 'star': character.star, 'enemy_attack': enemy_attack, 'enemy_defense': enemy_defense, 'enemy_hp': enemy_hp, 'max': max, # 允许在公式中使用max函数 'random': random.random } try: # 注意:生产环境慎用eval,此处仅为演示数据驱动思想。 damage_to_enemy = int(eval(damage_formula, {"__builtins__": {}}, context)) damage_to_enemy = max(0, damage_to_enemy) # 伤害非负 except Exception as e: print(f" 错误:伤害公式计算失败 '{damage_formula}' - {e}") damage_to_enemy = max(0, character.attack - enemy_defense) # 降级方案 # 3. 简单战斗模拟(实际项目会更复杂,可能有多个回合、技能) enemy_hp -= damage_to_enemy print(f" {character.name} 对敌人造成 {damage_to_enemy} 点伤害。") if enemy_hp <= 0: print(f" 敌人被击败!") # 4. 掉落奖励 loot = self._generate_loot(enemy_type) total_loot['gold'] += loot.get('gold', 0) if loot.get('potion_small', 0) > 0: total_loot['potion_small'] = total_loot.get('potion_small', 0) + loot['potion_small'] print(f" 获得了 {loot['potion_small']} 瓶小型药水。") else: print(f" 敌人剩余 HP: {enemy_hp} (本次模拟简化,假设角色获胜)") # 5. 结算奖励 if total_loot['gold'] > 0: character.money += total_loot['gold'] print(f"\n战斗结束,总计获得 {total_loot['gold']} 金币。") print(f"{character.name} 当前金币:{character.money}") def _generate_loot(self, enemy_type): loot_config = self.config.get('loot_table', {}).get(enemy_type, []) loot_result = {'gold': 0} for drop in loot_config: if random.random() <= drop.get('probability', 0): item = drop.get('item') amount_range = drop.get('amount', [0, 0]) amount = random.randint(amount_range[0], amount_range[1]) if isinstance(amount_range, list) else amount_range loot_result[item] = loot_result.get(item, 0) + amount return loot_result

8. 第五步:主程序串联与运行

现在,我们将所有系统串联起来,模拟标题描述的完整流程。

主程序入口 (main.py)

# main.py from models.character import Character from engines.growth_engine import GrowthEngine from engines.combat_engine import CombatEngine from utils.config_loader import ConfigLoader def main(): print("=== 游戏逻辑配置化演示:从‘打黑赛’到‘炼化小啰啰’ ===\n") # 1. 创建主角“潇龙飞” xiao = Character("潇龙飞") print(f"初始状态: {xiao}\n") # 2. 打黑赛赢钱(经济系统) print("--- 阶段一:参与地下黑赛 ---") # 假设连胜了2场 award_tournament_prize(xiao, 'black_tournament', win_streak=2) print() # 3. 使用金钱进行渡阶、升星、升重(成长系统) print("--- 阶段二:消耗资源进行成长 ---") # 注意:这里简化了资源消耗逻辑,实际需要检查金钱是否足够并扣除。 growth_engine = GrowthEngine() # 目标:升到 2阶 43星 100重 target_tier = 2 target_star = 43 target_refinement = 100 growth_engine.apply_growth_to_character(xiao, target_tier, target_star, target_refinement) print() # 4. 去炼化(战斗)几个小啰啰 print("--- 阶段三:挑战敌人 ---") combat_engine = CombatEngine() # “韩服几个小啰啰” 我们定义为一种普通敌人 combat_engine.resolve_combat(xiao, 'common_enemy', enemy_count=3) print("\n=== 流程模拟结束 ===") print(f"最终状态: {xiao}") # 这里需要定义或导入 award_tournament_prize 函数 def award_tournament_prize(character, tournament_key, win_streak=0): # 为演示,简单实现。实际应从经济系统配置读取。 config = ConfigLoader.get_economy_config() tournament = config.get('tournaments', {}).get(tournament_key) if tournament: reward = tournament.get('base_reward', 0) + tournament.get('reward_per_win_streak', 0) * win_streak character.money += reward print(f"{character.name} 参加了【{tournament.get('name', tournament_key)}】,赢得 {reward} 金币。") else: print(f"赛事 {tournament_key} 配置不存在。") if __name__ == "__main__": main()

9. 运行结果与效果验证

运行python main.py,预期会看到类似以下的输出,清晰地展示了数据在各个系统间的流动:

=== 游戏逻辑配置化演示:从‘打黑赛’到‘炼化小啰啰’ === 初始状态: 潇龙飞 [Lv: 0阶0星0重] ATK:100 DEF:50 HP:1000 Money:0 --- 阶段一:参与地下黑赛 --- 潇龙飞 参加了【地下黑赛】,赢得 7000 金币。 当前金币:7000 --- 阶段二:消耗资源进行成长 --- 潇龙飞 成长至 2阶43星100重。 属性变化: ATK 100 -> 476, DEF 50 -> 179, HP 1000 -> 2206 --- 阶段三:挑战敌人 --- 潇龙飞 遭遇了 3 个【common_enemy】。 > 战斗回合 1: 潇龙飞 对敌人造成 446 点伤害。 敌人被击败! 获得了 1 瓶小型药水。 > 战斗回合 2: 潇龙飞 对敌人造成 446 点伤害。 敌人被击败! > 战斗回合 3: 潇龙飞 对敌人造成 446 点伤害。 敌人被击败! 战斗结束,总计获得 350 金币。 潇龙飞 当前金币:7350 === 流程模拟结束 === 最终状态: 潇龙飞 [Lv: 2阶43星100重] ATK:476 DEF:179 HP:2206 Money:7350

如何验证系统工作正常?

  1. 修改配置,观察变化:打开configs/growth.json,将star_configstar: 1attack_multiplier1.1改为2.0。重新运行程序,你会发现角色的攻击力会大幅提升。这证明了策划可以独立调整数值,而无需程序员介入。
  2. 修改公式,观察战斗:打开configs/combat.json,将damage_formula_v2改为(attack * 2 - enemy_defense)。重新运行,战斗伤害会立即改变。这证明了战斗逻辑的可配置性。

10. 常见问题与排查思路

在实现此类数据驱动系统时,你可能会遇到以下问题:

问题现象可能原因排查方式解决方案
程序启动时读取配置失败,报JSONDecodeError1. JSON 文件格式错误(如缺少逗号、引号)。
2. 文件编码不是 UTF-8。
1. 使用 JSON 在线校验工具检查配置文件。
2. 检查文件是否以 UTF-8 编码保存。
1. 修正 JSON 语法错误。
2. 将文件另存为 UTF-8 编码。
角色属性计算不正确,与预期值偏差大1. 配置数据错误(如加成值填错)。
2. 属性计算公式有误(顺序、加减乘除)。
3. 配置查找逻辑有 bug(如未找到对应等级配置)。
1. 打印出每一步计算中的中间变量值。
2. 检查_calculate_tier_bonus等方法的查找逻辑。
3. 核对配置文件的键名与代码中读取的键名是否一致。
1. 修正配置数据。
2. 复核并修正计算公式。
3. 确保配置键名与代码引用完全匹配。
修改配置文件后,游戏内数值没有变化1. 配置没有被重新加载(程序缓存了旧配置)。
2. 修改了错误的配置文件。
3. 程序逻辑有缓存机制。
1. 确认修改后保存了文件。
2. 重启应用程序或触发配置重载。
3. 检查ConfigLoader是否实现了缓存及缓存更新策略。
1. 实现配置文件的热重载机制(如监听文件变化)。
2. 在开发阶段,每次测试前重启服务。
使用eval解析战斗公式存在安全风险eval可以执行任意代码,如果配置来源不可信,极其危险。审查配置文件的来源和权限。生产环境务必替换eval。可使用:
1. 安全的第三方库如asteval
2. 自己实现一个简单的数学表达式解析器。
3. 使用 Lua/Javascript 等脚本引擎隔离运行。
配置繁多,难以管理配置文件数量多,关系复杂,容易出错。审视配置项是否过度设计或存在冗余。1. 使用配置编辑工具(如内部工具或 Excel 导出 JSON)。
2. 建立配置版本管理和校验规则。
3. 考虑使用数据库或专业的配置中心管理。

11. 最佳实践与工程建议

将上述Demo思想应用到真实的游戏项目(如Unity)中,你需要考虑更多工程化细节:

  1. 配置管理进阶

    • 使用 ScriptableObject (Unity):对于Unity开发者,ScriptableObject是天生为数据驱动设计的资产类型,支持编辑器内可视化编辑和运行时高效读取。
    • 配置校验:编写编辑器脚本或独立工具,在策划提交配置时自动校验数值范围、引用完整性(如物品ID是否存在)、公式语法等。
    • 分表与合并:将配置按系统拆分(角色成长表、物品表、技能表),通过唯一ID关联。使用工具将多个Excel表合并、转换为游戏可读的二进制或JSON格式,以优化加载速度。
  2. 公式系统的安全与性能

    • 绝对避免生产环境使用eval。可以采用“字典+操作符”的方式预定义公式,例如:
      # 预定义公式库 FORMULA_LIB = { 'damage_v1': lambda a, d, s, ed: max(0, a * (1 + 0.05 * s) - ed * 0.7), 'heal_v1': lambda i, t: i * 0.5 + t * 0.1, } # 配置中只存储公式名 # "damage_formula": "damage_v1"
    • 或者集成轻量级脚本语言(如LuaPythonrestricted环境),实现更复杂、更安全的逻辑。
  3. 架构设计

    • 区分逻辑与数据:确保Engine(引擎)类只负责计算,不持有状态。所有状态(如角色属性)由Model(模型)类持有。
    • 依赖注入:通过依赖注入容器来管理GrowthEngineCombatEngine等服务的生命周期和依赖关系,提高可测试性。
    • 事件驱动:当角色升级、获得物品时,使用事件总线(Event Bus)通知其他系统(如成就系统、UI系统),而不是直接调用,降低模块耦合度。
  4. 性能优化

    • 缓存计算结果:对于频繁查询且不变的配置(如每级属性),在内存中建立快速查询表(Lookup Table)。
    • 异步加载:大型配置在游戏启动时异步加载,避免卡顿。
    • 增量更新:支持配置的增量热更新包,减少玩家下载量。
  5. 团队协作

    • 建立配置规范:与策划约定配置文件的命名、结构、字段类型和注释规范。
    • 版本控制:将配置文件纳入 Git 等版本控制系统,便于追溯更改和协作。
    • 提供可视化工具:为策划提供简单的编辑界面,降低他们直接编辑JSON/Excel的门槛和出错率。

通过以上步骤,我们从一个看似随意的标题出发,构建了一个高度可配置、易于维护的游戏逻辑框架原型。这套方法的核心在于将易变的游戏规则剥离出核心代码,用数据来控制行为。无论是调整“潇龙飞”从43星升到44星的具体数值,还是修改“炼化”小啰啰的伤害算法,都只需要动动配置文件,再也不用劳烦程序员深入代码腹地,更不用担心引发难以预料的BUG。这,就是数据驱动设计为现代游戏开发带来的真正自由与效率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询