2025 07 08 碧轨数值设计推导及反拆(ongoing)
碧轨数值设计推导及反拆 问题拆解 若需要为碧轨设计数值,我们需要从经济数值和战斗数值两方面进行问题拆解。其中,经济数值将会决定玩家打怪掉落、任务奖励数值、宝箱内容等与游戏内货币相关的数值;战斗数值将会决定玩家战斗时的攻击/防御、升级经验值需求、道具回复量等与战斗相关的数值。 经济数值 确认反馈逻辑以及游戏内经济系统类型 游戏内的反馈逻辑为:素材获取 - 养成提升 - 战斗验证。在这类单机游戏中,玩家的资源入口为固定任务奖励发放和可重复刷取的打怪掉落。玩家的资源出口为有上限的战力提升和收藏品收集。所以我们需要实现的目标是入口与出口平衡,并控制玩家在游戏过程中的刷取时间以平衡玩家游戏行为占比。 正向推导 我们假设采用基于玩家游玩时间的设计,\(产出价值 = 等级 \times 游玩时间 \times 模块倍率\)。并确定玩家在不同模块内的游玩时间。更重要的模块,例如主线任务,会拥有更高的模块倍率,从而使得玩家完成主线任务的意愿更高。玩家若需要在10级花费10分钟完成主线任务(模块倍率2.0),那么该主线任务的总奖励应为200单位价值(包括主线遇敌的掉落价值、任务奖励价值)。 将这些模块的产出价值加总便可得到玩家在一段游玩时间内的总产出价值。玩家会需要将这些价值投入到资源出口处。根据不同出口所占比例,我们可以估计出每个模块对于玩家资源的消耗。接下来,我们根据不同出口的类型进行价值计算: 永久,不可重复刷取(例:武器装备):若玩家提升武器模块的方式只有购买武器这一项,那么我们可以通过单机RPG武器定价的方式来计算武器的价值。 永久,可重复刷取(例:圣遗物、词条):我们可以通过计算玩家时间投入的方式来计算一次随机的价值。见单机RPG词条定价。 消耗品,不可重复获取(例:成就类收藏品):我们可以根据预期目标达成时间来计算这些道具的价值。 消耗品,可重复获取(例:战斗道具):我们需要道具对于玩家行为的影响来估计这些道具的价值,见单机RPG战斗道具定价 反向推导 接下来,我们将简要分析碧轨一部分具体模块的数值与其之间的关系,并与前文的正向推导进行对比。 经验值系统 金币系统 [TODO]经验值系统 游戏中的经验值由且仅由击败怪物获得。 [TODO]金币系统 战斗数值 [TODO]正向推导 [TODO]反向推导 附带计算 单机RPG武器定价 假设前提 在游戏模块(具体章节)内, 玩家在满练度时的战力为原练度时的200% 共有4个战力提升模块,每个模块对于战力的提升相互独立且为乘算 玩家需要达到至少原练度的150%才能通过该模块。达到该战力的时间为30min 玩家预期的战力提升时间为60min,满练度为120min 玩家在主线中花费的时间为120min,为固定时间 玩家在原练度时等级为50,满练度时等级为60 武器设定提升计算 每个战力模块在满练度时的提升为2^{1/4}=18.92%,花费时间为30min 每个战力模块在保底练度时的提升为1.5^{1/4}=10.67%,花费时间为7.5min 我们希望随着玩家花费时间增加,提升逐渐减少,故函数形如f(t) = p_1 * 2^(p_2 * t) + p_3, 已知f(0) = 1, f(7.5) = 1.1067, f(30) = 1.1892,近似计算后得 f(t) = -0.1982 * 2^(-0.1487 * t) + 1.1982 % 1. Define the data points t_data = [0; 7.5; 30]; f_data = [1; 1.1067; 1.1892]; % 2. Define the model function f(t) = k*2^(a*t) + b % The parameter vector 'p' holds the coefficients: p(1)=k, p(2)=a, p(3)=b model = @(p, t) p(1) * 2.^(p(2) * t) + p(3); % 3. Provide an initial guess for the parameters [k, a, b] % From f(0)=1, we know k+b=1. If we guess k=1, then b=0. % A small positive 'a' is a reasonable start. p0 = [-0.165030382039, -0.2, 1.1892]; % 4. Call lsqcurvefit to find the best-fit parameters % Syntax: lsqcurvefit(function, initial_guess, x_data, y_data) p_fit = lsqcurvefit(model, p0, t_data, f_data); % 5. Display the results k = p_fit(1); a = p_fit(2); b = p_fit(3); fprintf('The solved parameters are:\n'); fprintf('k = %f\n', k); fprintf('a = %f\n', a); fprintf('b = %f\n', b); fprintf('\nThe final equation is: f(t) = %.4f * 2^(%.4f * t) + %.4f\n', k, a, b); 根据以上近似,我们可以得到玩家花费不同时间可得到的预期提升。且我们可以得到玩家的预期战力为f(15)^4 = 175.27% 我们可以根据玩家在单个战力模块的时间投入回报节点来安排其对应收益。(例如,在5分钟时获取饰品A1,在10分钟时获取饰品A2;若A1与A2相互冲突,A1的提升应为7.98%,A2的提升应为12.75%) 武器价值计算 以武器获取为例。若安排3档武器,1级为自带,2级为合格,3级为最强。那么1级武器的伤害结果为100%,2级为110.67%,3级为118.92%。2级需要7.5分钟获取,3级需要30分钟获取。 玩家在游戏过程中不断产生价值,其价值与游玩时间、游戏等级成正比。我们可以令每分钟产生对应等级的价值。其中,主线产生的价值翻倍以引导玩家行为。 玩家会将游戏过程中产生的价值以货币的形式投入于战力模块,从而提升自己的战力。 玩家在游戏模块中最高的花费时间为240min。其中,120min为主线,产出\(120 \times 50 \times 2=12000\)单位价值,120分钟为养成,产出\(120 \times 50=6000\)单位价值,共18000单位价值。这些价值的出口在于战力模块,也就是说每个战力模块可以被分配4500单位价值。 我们希望玩家无需重复刷取,可以通过3级武器=2级武器+升级包的形式设计武器购买。根据获取时间比例,2级武器为900单位价值,3级武器升级包为3600单位价值。若我们平滑武器强度曲线,令3级武器为19%提升,但令2级武器为14%提升,则可以通过前文中计算的公式来获取刷取时间:2级12分钟。那么此时2级武器为1800单位价值,3级武器升级包为2700单位价值,玩家的观感更好。 单机RPG圣遗物定价 假设前提 在该模块内, ...
2025 07 02 鸟群行为模拟
涌现式群体行为模拟 link: 【游戏开发秘籍】用算法让NPC集体“开窍”?Boids鸟群算法详解! 鱼群、人群、蜂群等的自然模拟,使用Boids算法。 算法逻辑 $$ \vec{V} = \Pi c_i \vec{v_i} $$其中,\(\vec{V}\)为最终向量,\(c_i\)为可调权重,\(\vec{v_i}\)为单规则向量。 我们有以下3条规则: 规则一:Cohesion, 向群体中心靠拢 目标:保持群体集聚。 行为:计算特定范围(通常为视野范围)内同伴的平均位置,然后产生一个朝向这个平均位置的驱动力。 规则二:Separation, 避免与同伴碰撞 目标:避免碰撞。 行为:对于每一个过近的同伴,产生一个排斥力,将这些排斥力加总。 规则三:Alignment, 与同伴移动方向保持一致。 目标:与群体行动方向趋同。 行为:计算特定范围(通常为视野范围)内同伴的平均方向,然后调整方向来匹配平均方向。 算法实现 import numpy as np import random import matplotlib.pyplot as plt import math import statistics import pygame MAX_SPEED = 10.0 WEIGHT_COHESION = 1 WEIGHT_SEPARATION = 1 WEIGHT_ALIGNMENT = 1 WEIGHT_FORCE_AGAINST_WALL = 0 SOCIAL_DISTANCE = 20.0 FRAMERATE = 30 GRAY = (127, 127, 127) WHITE = (255, 255, 255) def cart2pol(x, y): rho = np.sqrt(x**2 + y**2) phi = math.degrees(np.arctan2(y, x)) return((phi + 360) % 360, rho) def pol2cart(phi, rho): x = rho * np.cos(math.radians(phi)) y = rho * np.sin(math.radians(phi)) return(x, y) class EmergenceBehaviorDemo: class Unit: x = 0.0 y = 0.0 phi: float v = 0.0 i: int def __init__(self, i): self.i = i class Playground: w = 1280.0 h = 720.0 def __init__(self): pass unit_index: int peasants: list[Unit] playground: Playground leader_id: int leader_walk_weight_of_previous_tick: float = 0.6 plot_x: list[float] plot_y: list[float] def init_peasants(self, group_grid_width: int, playground: Playground, interval: float) -> list[Unit]: # Generate at the center of the playground class Center: x = playground.w/2 y = playground.h/2 class StartingCoord: x = Center.x - ((group_grid_width - 1) * interval / 2) y = Center.y - ((group_grid_width - 1) * interval / 2) self.unit_index = 0 peasants = [] for i in range(group_grid_width): for j in range(group_grid_width): new_unit = self.Unit(self.unit_index) new_unit.x = StartingCoord.x + (interval * i) new_unit.y = StartingCoord.y + (interval * j) new_unit.phi = random.random() * 360 peasants.append(new_unit) self.unit_index += 1 return peasants @classmethod def print_peasants_coordinates(cls, l:list[Unit]): for unit in l: print(f"Peasant {unit.i}: ({unit.x}, {unit.y}), v = {unit.v}") return @classmethod def print_peasant_coordinates(cls, u:Unit): print(f"Peasant {u.i}: ({u.x}, {u.y}), phi = {u.phi}° v = {u.v}") return @classmethod def update_position(cls, u: Unit, p: Playground): u.x += u.v * math.cos(math.radians(u.phi)) u.y += u.v * math.sin(math.radians(u.phi)) u.x = u.x % p.w u.y = u.y % p.h @classmethod def point_in_triangle(cls, p, t_1, t_2, t_3): # https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle def sign(p_1, p_2, p_3): return (p_1[0] - p_3[0]) * (p_2[1] - p_3[1]) - (p_2[0] - p_3[0]) * (p_1[1] - p_3[1]) d1 = sign(p, t_1, t_2) d2 = sign(p, t_2, t_3) d3 = sign(p, t_3, t_1) has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) return not (has_neg and has_pos) @classmethod def get_distance(cls, p1, p2): return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) def get_all_unit_in_eye_sight(self, u: Unit, fov: float = 90.0, social_distance: float = 20.0, view_distance: float = 300.0) -> list[Unit]: res = [] vertex_1 = (u.x, u.y) vertex_2 = (u.x + math.cos(math.radians(u.phi+(fov / 2))) * view_distance, u.y + math.sin(math.radians(u.phi+(fov / 2))) * view_distance) vertex_3 = (u.x + math.cos(math.radians(u.phi-(fov / 2))) * view_distance, u.y + math.sin(math.radians(u.phi-(fov / 2))) * view_distance) # Now we have one triangle for peasant in self.peasants: if peasant.i == u.i: continue if self.point_in_triangle((peasant.x, peasant.y), vertex_1, vertex_2, vertex_3) or self.get_distance((u.x, u.y), (peasant.x, peasant.y)) <= social_distance: res.append(peasant) return res def get_all_unit_nearby(self, u: Unit, fov: float = 90.0, social_distance: float = 50.0, view_distance: float = 300.0) -> list[Unit]: res = [] # Now we have one triangle for peasant in self.peasants: if peasant.i == u.i: continue if self.get_distance((u.x, u.y), (peasant.x, peasant.y)) <= social_distance: res.append(peasant) return res @classmethod def combine_force_list(cls, l_force: list, l_weight: list = []): final_force_cart = [0, 0] if len(l_weight) == 0: actual_weight = [1] * len(l_force) else: actual_weight = l_weight for i in range(len(l_force)): (x, y) = pol2cart(l_force[i][0], l_force[i][1]) final_force_cart[0] += x * actual_weight[i] final_force_cart[1] += y * actual_weight[i] final_force_rad = cart2pol(final_force_cart[0], final_force_cart[1]) return final_force_rad @classmethod def distance_between_point_and_line(cls, _lv1, _lv2, _p): lv1 = np.array(_lv1) lv2 = np.array(_lv2) p = np.array(_p) return np.abs(np.cross(lv2-lv1, lv1-p)) / np.linalg.norm(lv2-lv1) @classmethod def force_against_the_walls(cls, u: Unit, p: Playground): v1 = (0.0, 0.0) v2 = (0.0, p.h) v3 = (p.w, 0.0) v4 = (p.w, p.h) s = (u.x, u.y) c = 200.0 # Wall 1 v1-v2 d1 = cls.distance_between_point_and_line(v1, v2, s) f1 = (90.0, c / d1) # Wall 2 v1-v3 d2 = cls.distance_between_point_and_line(v1, v3, s) f2 = (180.0, c / d2) # Wall 3 v4-v2 d3 = cls.distance_between_point_and_line(v4, v2, s) f3 = (0.0, c / d3) # Wall 4 v4-v3 d4 = cls.distance_between_point_and_line(v4, v3, s) f4 = (270.0, c / d4) force_against_the_walls_list = [f1, f2, f3, f4] force_against_the_walls = cls.combine_force_list(force_against_the_walls_list) return force_against_the_walls def find_peasant_with_id(self, i): for u in self.peasants: if u.i == i: return u raise Exception('no such id') def update_behavior( self, cohesion_weight: float = WEIGHT_COHESION, separation_weight: float = WEIGHT_SEPARATION, social_distance: float = SOCIAL_DISTANCE, alignment_weight: float = WEIGHT_ALIGNMENT, force_against_wall_weight: float = WEIGHT_FORCE_AGAINST_WALL ): new_direction_list = [] for peasant in self.peasants: peasants_in_view = self.get_all_unit_in_eye_sight(peasant) peasants_nearby = self.get_all_unit_nearby(peasant) # Calculate cohesion if len(peasants_nearby) == 0: cohesion_force = (0, 0) else: average_position_in_view = ( statistics.mean(map(lambda x: x.x, peasants_nearby)), statistics.mean(map(lambda x: x.y, peasants_nearby)) ) (xs, ys) = pol2cart(peasant.phi, peasant.v) cohesion_force = cart2pol((average_position_in_view[0]-xs), (average_position_in_view[1]-ys)) # Calculate Separation separation_force_list = [] for u in self.peasants: if u.i == peasant.i: continue if self.get_distance((u.x, u.y), (peasant.x, peasant.y)) <= social_distance: c = -1 / math.sqrt((u.y - peasant.y)**2 + (u.x - peasant.x)**2) (x, y) = ((u.x - peasant.x) * c, (u.y - peasant.y) * c) new_force = cart2pol(x, y) separation_force_list.append(new_force) if len(separation_force_list) == 0: separation_force = (0, 0) else: separation_force = self.combine_force_list(separation_force_list) # Calculate Alignment if len(peasants_in_view) == 0: alignment_force = (0, 0) else: average_movement_in_view = ( statistics.mean(map(lambda x: x.phi, peasants_in_view)), statistics.mean(map(lambda x: x.v, peasants_in_view)) ) (xt, yt) = pol2cart(average_movement_in_view[0], average_movement_in_view[1]) (xs, ys) = pol2cart(peasant.phi, peasant.v) alignment_force = cart2pol((xt-xs), (yt-ys)) # Calculate force against the wall force_against_wall = self.force_against_the_walls(peasant, self.playground) cohesion_force = (cohesion_force[0], cohesion_force[1]**0.75) alignment_force = (alignment_force[0], math.sqrt(alignment_force[1])) total_force = self.combine_force_list( [cohesion_force, separation_force, alignment_force, force_against_wall], [cohesion_weight, separation_weight, alignment_weight, force_against_wall_weight] ) if peasant.i == self.leader_id: print(f"c_force:({cohesion_force[0]:.2f}°, {cohesion_force[1]:.2f}m/s) s_force:({separation_force[0]:.2f}°, {separation_force[1]:.2f}m/s) a_force:({alignment_force[0]:.2f}°, {alignment_force[1]:.2f}m/s) f_force:({force_against_wall[0]:.2f}°, {force_against_wall[1]:.2f}m/s)") new_direction_list.append((peasant.i, total_force)) for (_i, (_phi, _v)) in new_direction_list: current_peasant = self.peasants[_i] current_peasant.phi = _phi current_peasant.v = min(_v, MAX_SPEED) for peasant in self.peasants: self.update_position(peasant, self.playground) def __init__(self, group_grid_width: int, interval: float): self.playground = self.Playground() self.peasants = self.init_peasants(group_grid_width, self.playground, interval) self.leader_id = random.randint(0, len(self.peasants)-1) self.plot_x = [] self.plot_y = [] pass def pygame_rot_center(image, angle, x, y): rotated_image = pygame.transform.rotate(image, angle) new_rect = rotated_image.get_rect(center = image.get_rect(center = (x, y)).center) return rotated_image, new_rect def main(): run = True game = EmergenceBehaviorDemo(7, 10.0) pygame.init() window = pygame.display.set_mode((game.playground.w, game.playground.h)) clock = pygame.time.Clock() pygame_image_fish = pygame.image.load('fish.png').convert_alpha() while run: clock.tick(FRAMERATE) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False game.update_behavior() window.fill(GRAY) for peasant in game.peasants: if peasant.i == game.leader_id: pygame.draw.rect(window, WHITE, (peasant.x - 5, peasant.y + 5, 10, 10)) else: rotated_image, new_rect = pygame_rot_center(pygame_image_fish, (peasant.phi + 270) % 360, peasant.x - 5, peasant.y + 5) window.blit(rotated_image, new_rect) pygame.display.flip() plt.plot(game.plot_x, game.plot_y, color='blue') plt.grid(True) plt.savefig('foo.png') pygame.quit() main()
2025 06 27 数值笔记
Markov Chain Problems 问题1:解谜时间 你面前有3扇门, 第一扇门,进入概率25%,进入后消耗 5 分钟返回。 第二扇门,进入概率25%,进入后消耗 10 分钟返回。 第三扇门,进入概率50%,进入后消耗 6 分钟后离开。 求离开的期望时间。 解: 进入第一第二扇门后,都会回到初始状态。也就是说返回后依然需要花费期望时间 $E$ 来离开。所以我们可以列出一下等式: $$ E = 0.25 (5 + E) + 0.25 (10 + E) + 0.5 * 6 $$求得 $$ E = 13.5 $$问题2:暴击预期打击数 你面前有1个100血的小怪,你每次击打它时: 50% 造成1点伤害。 50% 造成2点伤害。 请问期望需要多少次击杀? 解: 这是一个Markov Chain的Mean Absorbing Time问题 假设我们有101个状态,其对应的预期变幻次数为 \(\mu_i\), 易得 $$ \begin{aligned} \mu_0 &= 0 \\ \mu_1 &= 1 \\ \mu_n &= 1 + 0.5 \mu_{n-1} + 0.5 \mu_{n-2} \end{aligned} $$接下来我们需要将其转换为通项公式,首先利用待定系数法构造等差数列: ...
地牢内宝箱经验分布设计
第一个问题:设计一个地牢内宝箱的经验分布 我们使用基于玩家探索地牢行为的设计。关注玩家接触到该宝箱时的时间以及预期等级,并以此为自变量设计单个宝箱内经验量。从而推算玩家升级所需经验值。此时,玩家的等级可以被抽象理解为对地牢的探索程度。 具体流程如下: 计算宝箱权重:我们首先确定每个宝箱被打开所花费的时间,玩家打开该宝箱时的预期等级,将两者相乘便可得到该宝箱打开难度的权重。权重越高,代表该宝箱获取的成本越高。我们将基于该权重设计玩家升级所需的经验值。 确定升级路径:我们需要具体关注玩家在每一级时需要打开的宝箱数量,从而确保玩家的升级过程平滑。假设整个地牢内有30个宝箱,其中70%是95%以上玩家通过时会打开的(我们需要保证玩家在错过其中一些宝箱的前提下依然能正常进行游戏);且随着玩家等级的增加,需要打开的宝箱数量线性增加。此时我们得到了玩家每次升级时需要打开的宝箱数量。 推算升级经验:基于玩家升级时需要打开的宝箱数量,我们将特定等级宝箱中打开难度权重较低的对应数量加总,便可以得到玩家在该等级时升级所需要的预期经验值。 该设计的优势: 避免“越级打怪”或经验不足的问题,确保玩家在正常探索路径中即可满足升级条件。 控制经验冗余,避免玩家通过全图探索获取超量经验。 即使玩家通过非预期解提前开启高等级宝箱,也不会因经验获得而打破系统平衡。 衍生问题: 对于已经深入迷宫的玩家而言,前期岔路内的高级宝箱比后期主线宝箱所需要的获取时间长,此时如何平衡获取难易度与宝箱经验之间的关系?我的想法是可以为前期较难获取的宝箱设计额外的,与升级无关的奖励(如称号等),从而给玩家提供成就感。 第二个问题:基于上题,设计玩家装备随等级提升的曲线,以及对应等级怪物的数值曲线 我们基于玩家时间投入,关注玩家的战斗时间消耗以及玩家的战斗频率,以此为确定玩家的单次战斗时间。之后固定玩家的秒伤曲线以确保养成带来的反馈观感后,根据单次战斗时间推算玩家的数值以及怪物的血量。再基于玩家装备提供数值在总数值中的占比,推算玩家的装备随等级提升的曲线。 具体流程如下: 确定单次战斗时间:我们首先确定玩家每一级的升级所需时间、战斗时间在其中的占比、以及升级所期望的战斗频率。从而得到玩家在具体等级期望的单次平级战斗时间。 预设秒伤曲线:我们利用指数函数以及秒伤初始值预设玩家的秒伤。这里我使用的函数为秒伤 = 20*1.071^(玩家等级-1),即每提升10级玩家的秒伤翻倍,提供给玩家稳定的成长反馈。 确定怪物血量:根据玩家秒伤与单次平级战斗时间确定怪物的血量。 推算装备成长曲线:根据玩家秒伤确定玩家的数值(具体攻击力、暴击等)。之后,根据玩家装备在数值中的预期占比,确定玩家装备随等级提升的曲线。 额外考虑要素: 平级、压级、越级战斗:玩家在游戏过程中会不可避免的遇到压级或越级战斗。为保证玩家体验,我们需要根据目标玩家群体来确定玩家的越级能力(动作类游戏偏高,回合制游戏偏低)。 boss的额外奖励、分段函数:玩家在击败boss后,会预期获取一个与其难度对应的奖励,若该奖励被设计为战力奖励,那么玩家会预期获得一个较大的战力提升。此时如果使用较为平滑的曲线,玩家将会难以察觉到自己的战力提升。我们可以使用分段函数来确定玩家的秒伤,如秒伤 = 20*1.071^(玩家等级-1)*(1.33^ROUNDDOWN(玩家等级/10,0)),此时玩家每过10级会有一个额外的33%提升,令玩家打过boss后的正反馈更加明显。使用这一公式还会增强破序惩罚:若玩家在打过第一个地牢的boss前就去尝试挑战第二个地牢,由于第二个地牢怪物战力是以玩家击败第一个boss为前提设计的,其战力不足会更容易暴露,从而引导玩家按照预期路线推进游戏。 装备迭代的压力:获取装备需要消耗时间成本,若玩家获取装备的流程不能耦合在玩家的升级过程中,那么玩家便需要频繁进行额外的操作来赶上怪物等级提升的速度
QQ14经验值随笔
如果要我去填QQ14的经验值曲线,我会怎么填? 基于玩家游戏时间的经验值设计思路 一切从玩家游戏时间开始。玩家在QQ14的游戏时间主要由副本与任务构成。对于每名玩家的第一个职业而言,玩家的前期时间完全由任务构成;中期由完成任务为主导,攻略副本为辅助;后期则由攻略副本为主导,完成任务为辅助。 我们的经验公式大致如下所示: $$升级所需经验值(等级) = \frac{\sum{该等级主线任务经验}}{主线任务占该等级游玩体验比(等级)}$$$$单个任务经验 = 等级 \times 任务完成时间$$可能面临的问题以及其他细节 1. 开风脉、开地图、击杀小怪等杂项经验如何填写? 这些经验的填写核心与预期副职业的升级效率与第一职业的升级效率比有关。 玩家主要会在提升副职业等级时利用到这些杂项经验,所以若我们希望玩家提升副职业等级时的效率为主职业的1/4,那我们应该把这些杂项经验的公式后乘上1/4。我们预期玩家在不采用一次性经验时提升副职业等级所花费的总时间为提升主职业时的4倍。 我们将杂项经验按照其可获取数量分为两类: 一次性经验(探索) 可重复刷取经验(战斗、fate等) 这些经验应该与预期玩家获取时的等级以及其在玩家经验构成中的占比相关,而具体的经验计算应该与预期玩家行为相关。而这些杂项经验的经验获取效率应该比玩家正常完成主线要低很多,其中可重复刷取经验会更低。 举例来说,探索经验与探索行为相关。玩家通过在大世界花费时间移动来探索,花费时间越多理应获取经验越高,所以探索经验应该与最短移动时间成正比。由于玩家可以在大地图中任意切换职业,我们希望避免玩家切换低级职业领取高级地图探索奖励的情况,所以获取的探索经验应该和玩家探索时采用的职业等级成正比。最后,我们需要乘上相比副职业的升级效率常数更高的常数(因为一次性的资源更加稀缺)。得到如下公式: $$单区域探索经验 = 玩家目前等级 \times 最短移动时间 \times 0.5$$2. 攻略副本的经验具体如何填写? 攻略副本的经验获取效率应低于完成主线的经验获取效率。我们预期玩家提升副职业时的主要经验获取为攻略副本。以玩家游戏时间为基础,我们可以很容易的得到以下公式: $$副本总经验 = 预期玩家等级 \times 预期完成时间 \times 0.25$$而日常任务的获取效率会是更高的,需要将最后的常数提高。 3. 主线任务占该等级游玩体验比如何设计? 我们将玩家的战斗过程分为以下几个阶段: 熟悉游戏阶段 适应游戏阶段 (高难挑战阶段) 玩家的等级提升应该在熟悉游戏阶段到适应游戏阶段之间。所以玩家的游玩体验中,随着等级的逐渐提升,主线任务的占比应该逐渐降低。对于游戏前期而言,我们希望玩家升级的门槛尽可能低,以增加玩家留存率。所以对于1级的玩家,主线任务的占比可以被设为100%。对于最后一级而言,主线任务的占比应该根据玩家主线所有副本经验计算得到。计算的目的是玩家完成全部主线任务并第一遍通关主线副本可以获得足够的经验升至满级。而中期的经验占比可以使用线性函数获取。 4. 没有主线的等级? 在QA过程中,我们需要避免以下情况:玩家在做出了必要最少程度的经验获取,之后在某一个等级无法通过主线和必要的副本获取足够量的经验接取下一级的主线。这一情况很可能会打破玩家的心流,从而降低玩家的留存概率。 5. 经验值的观感? 基于前文中提到的经验值计算,我们有可能会面临经验值需求不随着等级增长的情况。当一个等级中的主线任务耗时过长,便可能会诱发这种情况。我们可以通过和策划沟通修改部分主线任务的预期等级来解决这个问题,也可以通过计算最后规范化数值来解决这个问题。前者是一个更好的解决方案,后者在实现过程中则需要注意修改其他经验获取途径的经验值获取。
碧蓝幻想战斗数值的设计推导以及反拆
❗ This article was written in 2025.04 ❗ 前言 Granblue Fantasy(以下简称GBF)是一款网页端/移动端长线运营小队战斗RPG,除去其较高的美术水平外,核心乐趣点在于养成所带来的战斗数值提升。GBF运营至今能够通过其数值设计不断给玩家提供完成每日任务、参与活动的动力,说明其数值设计值得我们对此进行推导拆解分析。 GBF战斗系统说明 GBF的战斗系统以小队为单位,每个小队由【主角】、3名【角色】、【武器盘】、【召唤石】构成。武器盘和召唤石决定了玩家的基础伤害。特殊的武器盘和召唤石会提供一些效果影响伤害的结算。 实际战斗时为回合制战斗,每回合玩家可以使用主角或角色的技能、召唤石的技能,在使用想要使用的技能之后玩家点击攻击结束自己的操作;之后主角和其他角色依次进行一次普通攻击,最后怪物行动。 战斗公式设计推导 目标以及场景拆解 GBF数值设计的目标主要包括以下两点: 【保证数值主导的正反馈循环】因为战斗是游戏的主体,所以需要保证玩家在时间游玩中,对时间和资源投入的成长有足够的感知。为此,我们希望满足以下目标: 玩家经过养成,能够获得可感知的战斗数值提升 战斗数值的膨胀应该对于长线运营有较高的兼容度 【确保不同氪金量玩家体验】氪金玩家对于非氪金玩家不应该形成完全碾压 战斗场景: 完全没有PVP。PVE场景包括多人或单人。 玩家的养成主要通过多人PVE。 玩家的成果检验主要通过单人PVE。 数值框架搭建 基于目标以及场景,我们给出一个基于养成模块的伤害计算公式。 $$ \begin{aligned} 伤害 =&\ (预期伤害 \times 乘算调整区 + 加算调整区) \times 最终乘算调整区 + 最终加算调整区 \\ \text{其中,} \\ 预期伤害 =&\ 养成伤害 \times 怪物减伤率 \\ =&\ \Pi_{养成模块} 单模块调整值 \times 怪物减伤率\\ \end{aligned} $$默认乘算调整区、加算调整区、最终乘算调整区、最终加算调整区不影响伤害结算。 由于玩家的战斗场景均为PVE,故可以通过手动投放怪物减伤率的方式控制玩家体验(默认10%减伤,特殊战斗特殊对待)。 观察游戏机制,细化计算公式 观察到: 游戏内有主角等级、武器盘、召唤石等养成模块。 武器盘内的武器可以提供各类效果增幅伤害,召唤石可以增幅武器盘内武器的效果,或直接增幅伤害。 战斗中有部分技能可以增幅伤害,或给怪物增加"破防XX%“的debuff。 战斗中不同技能的倍率不同,且可以出现暴击 【特殊机制】战斗中有元素克制关系 根据观察到的结果,我们对养成伤害、怪物减伤调整区公式进行细化: $$ \begin{aligned} 乘算调整区 =&\ \Pi_{乘算调整模块} 单模块调整值 \\ 乘算调整模块 =&\ \set{伤害倍率、元素克制调整值、暴击调整值} \\ 养成模块 =&\ \set{基础伤害、武器盘调整值、召唤石调整值} \\ 怪物减伤调整区 =&\ 1 - f(怪物减伤值) \end{aligned} $$模块计算方式假设 我们假设以下情况: ...
Scythe
❗ This article was written in 2021 ❗ SCYTHE 一、前言 《镰刀战争》是一款由Jamey Stegmaier设计的3.5X(探索、扩张、开发、半对抗)桌游,主题是在虚构的1920年代东欧大陆展开一场耕战并重的对抗。游戏支持1-5人,大部分情况下为2-4人。 作为一款2016年出版的非对称游戏,《镰刀战争》的美术设计以及经典的引擎构筑玩法收获了一批玩家的青睐。而它的缺点也较为明显:游戏结束触发条件不够合理、阵营之间强度差距较大、没有提供足够的平衡拓展空间等等。而这些缺点的存在值得我们进行分析与思考,并吸收这些错误经验。 在阅读本文之前,读者应该对游戏的规则有基础的认识。在本文写作中,为了方便,一部分用于可能与官方汉化版不同,以下为与官方汉化不同用词的对照表。 用词对照表 文内用词 官方汉化 招募 新兵 战力 战力值 领地、土地 领土 基础行动 上层行动 高级行动 下层行动 T 回合 阵营 势力 我们试图解决的问题 针对《镰刀战争》中的资源运作系统进行建模。算出《镰刀战争》中资源的价值。 在《镰刀战争》这个非对称游戏中,是否依然有称得上“过强”的阵营? 在《镰刀战争》中是否有不平衡的玩家板块? 二、资源的价值 首先我们从分析每一种资源的价值开始。我们需要一个标准单位来衡量一名玩家在做各种行动时得到的价值。在《镰刀战争》中,这个单位显然可以是金币。原因是在结算时,所有金币以外的资源要么废弃,要么就有一个途径来转换为金币。有这样一个标准单位之后,我们便可以分析各个阵营的初始价值、各个板块的初始价值、行动的收益等。有的时候,我们也将用“G”来表示金币以获得更好的可读性。 但需要注意的是,一个行动的实际价值取决于该场游戏的其他玩家、版图上的兵力分布等等条件。以下分析不基于任何具体的对局,仅供参考。 接下来的分析基于的前提是:设计时,工厂卡的基础行动是相对平衡的,具有稳定的收益,从而使得玩家更有动力去争夺地图中心。 我们可以列出所有的工厂卡,并排序: 1. 1金币 + 1战斗卡 -> 2生产 (生产只在该工厂卡中出现,所以暂时忽略) 2. 1声望 -> 1招募 / 1升级 3. 1声望 -> 1机甲 / 1建筑 4. 2不同资源 -> 1机甲 / 1建筑 5. 2不同资源 -> 1招募 / 1升级 6. 1战斗卡 -> 2声望 7. 1战斗卡 -> 3金币 8. 1战斗卡 -> 3任意资源 9. 1战斗卡 -> 1农民 + 2金币 10. 1战斗卡 -> 1升级 + 1战力 11. 1战力 -> 2声望 12. 1战力 -> 3金币 13. 2金币 -> 1升级 + 1声望 14. 2金币 -> 1机甲 + 1战力 15. 2金币 -> 1建筑 + 1声望 16. 2金币 -> 1招募 + 1战力 17. 1资源 -> 2战力 + 1战斗卡 18. 1资源 -> 1战力 + 1战斗卡 + 1声望 我们直接可以得到作为行动收益时,资源之间的关系: ...
Initial_post
Finally get things working! [x] test image uploading before writing more. Github Actions Remember to checkout using private token name: Release # Triggered by pushing on main branch on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout source uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 token: ${{ secrets.PRIVATE_TOKEN }} - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: latest - name: Build run: hugo --minify - name: Deploy if: github.ref == 'refs/heads/main' run: | cd public git config user.email '<your.email>' git config user.name '<your.name>' git add . git commit -m '<your.upload.commit>' git push --force origin HEAD:main