zoukankan      html  css  js  c++  java
  • 守望工坊 | 安娜:生物手雷的定点投掷

    背景

    假期的我总是无法用心学习,碌碌无为的日子里会玩一些游戏,譬如守望先锋。作为一名鱼塘(1.8k-2.1k)的辅助玩家,我总是希望能在喜爱的辅助中选择贡献更多的一位。安娜,可爱的人设、独特的机制、复杂情景下的适应力吸引了我,成为了我最常用的辅助角色。

    在最近的一次复盘中,我第一次见到了定点瓶的用法。好奇的我花费了数小时的时间为每张攻防地图寻找了合适的点位,这是一个繁琐又乏味的过程。坐标、角度、参照物的不确定性的叠加让我感到不安,那一夜我用被子将自己包裹得严严实实,只是因为这种情绪强化了我在寻找点位时游戏背景中的声音带来的恐惧。糟糕的经历令我产生了通过地图工坊减少一些不确定因素的想法,于是有了这篇文章。

    概览

    概括地说,在确定出发点与落点后,工坊的代码会为我们找到投掷的角度。工坊代码:Y2YRE

    基本用法

    1. 按下Q将当前位置设为落点
    2. 移动到安全区域后,按下SHIFT唤出并瞄准指示器。
    3. 投掷生物手雷,该手雷在不被阻挡的情况下一定会坠至落点

    辅助功能

    • 通过左键快速传送,以便放置点位,同时也能借此上高台。
    • 按住F可以暂时将镜头移至落点,以便观察生物手雷的轨迹。镜头的角度于设置落点时确定。
    • 唤出指示器后,代码将预测并报告生物手雷的滞空时间。

    演示视频

    设计思路

    验证抛物线猜想

    出于生活的惯性思维,我猜想生物手雷的轨迹是一段抛物线,并尝试验证。验证的过程需要两个参数:投掷速度((v_0))、重力加速度((g))。我尝试了最简单直观,且在这次尝试中取得了成功的方式:测量。

    • 投掷速度:竖直上抛,时间、重力加速度、初速度三者满足:(gt=2v_0)
    • 重力加速度:在国王大道A点三楼记录高度与自由落体的滞空时间,满足:(h=frac{1}{2}gt^2)

    至此,(v_0)(g)成为已知量。之后我设计了一个demo,在投掷手雷的同时绘制从玩家朝向出发,初速度为(v_0),加速度为(g)的抛物线。观察到抛物线与轨迹几乎完全重合,猜想成立。

    demo_screenshot

    导出投掷角度

    已知投掷角度的情况下绘制轨迹是非常直观且简单的,但我们的任务是通过起点、落点、初速度求出投掷角度。尽管用到的都是初等数学知识,后者却繁杂了许多。根据物理与数学的知识,列出等式,联立求解。虽然推导过程中犯了许多错误,但最终得出了正确答案。

    deduce_vx_vy

    下载pdf格式:链接:https://pan.baidu.com/s/1TJ5c-QSZGYHvmA0NPQLtBQ 提取码:meow

    导出滞空时间

    虽然在导出投掷角度的中间步骤中我们用到了所需的两段时间(t_s)(t_d),但是在实现的过程中它们已被消除。在已经求出(v_y)的情况下,滞空时间的计算非常简单。将抛物线的运动分为两部分:

    1. 从起点到最高点
    2. 从最高点到落点

    这两部分均可视作自由落体运动,对于前一段我们已知最大速度:(t_s=frac{v_y}{g});对于后一段我们已知竖直方向上的位移:(t_d=sqrt{frac{2(h_S+h)}{g}}),其中(h_s=frac{1}{2}gt_s^2)(h)为起点与落点在竖直方向上的高度差。最终用时为:(t_s+t_d)

    检查一致性

    注意到在导出投掷角度的过程存在对二次式的分类讨论,但在最终的表达式中该过程被消去。不难验证,这个算法的正确性不依赖起点与落点的高低关系。

    实现

    实践出真知,将设计转化为产品的过程总需克服一些困难。

    克服精度问题

    地图工坊的数字在(x)正半轴的表示范围为([10^{-3}, 10^8])。在测试时发现对一般规模的输入参数进行运算的过程中,部分中间值远大于(10^8) 。为了能够得到正确答案,我尝试将所有的中间值乘以(10^{-4}),这样可以将小数位利用起来。同时由于在玩家尺度下,(0.1)米的误差是可以接受的,我们乘以(10^{-4})而不是(10^{-3})。最终我们的代码满足了一般尺度下的求解需求。

    改善传送的穿模问题

    通过射线命中位置命令,我们可以得到玩家瞄准的目标坐标,但直接将玩家传送到该坐标是及其不负责任的。玩家有可能因穿模被传送到墙体后方、掉入地下、卡入墙体中。尝试了两种解决方案,各有优劣,最终选择了第二种。

    方案一 方案二
    改进 添加最近的可行走位置限制 通过射线命中法线修正传送点
    优点 玩家不会被传送到墙体内或地下 传送的位置精确符合玩家预期
    缺点 无法上高台,传送点误差较大 低概率卡入墙体

    代码

    希望有能力的朋友能将其引入外服,标注原作者本文地址即可。

    中文代码

    变量
    {
    	全局:
    		0: g
    		1: squv0
    		2: squg
    
    	玩家:
    		0: w
    		1: h
    		2: a_div_1000
    		3: squw
    		4: squh
    		5: delta_div_10000
    		6: b_div_10000
    		7: vhor
    		8: vy
    		9: v_hor_x
    		10: v_hor_z
    		11: dir_hor_eye_to_tar
    		12: pos_tar
    		13: flg_preview
    		14: dir_thrown_at
    		15: c_div_100000
    		16: id_indicator
    		17: id_target
    		18: dir_tar_eye
    		19: id_time_pred
    		20: time_pred
    		21: ts
    		22: hs
    		23: id_indicator_bottom
    }
    
    子程序
    {
    	0: find_direction
    	1: pred_time
    }
    
    规则("作者(Author):虐得我喵喵叫#5652")
    {
    	事件
    	{
    		持续 - 全局;
    	}
    
    	动作
    	{
    		等待(1, 无视条件);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("作者:虐得我喵喵叫#5652"), 左边, 32, 白色, 白色, 橙色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("5、按下主要攻击模式键传送。"), 左边, 6, 白色, 白色, 天蓝色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("4、任意时刻按住互动键进入落点视角。"), 左边, 5, 白色, 白色, 天蓝色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("3、将对准指示器并抛出手雷,手雷将坠至落点。"), 左边, 4, 白色, 白色, 天蓝色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("2、按下麻醉镖键唤出/刷新指示器(红色小球)。"), 左边, 3, 白色, 白色, 天蓝色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("1、按下终极技能键设置当前位置为落点。"), 左边, 2, 白色, 白色, 天蓝色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("帮助:"), 左边, 1, 白色, 白色, 天蓝色, 可见和字符串, 默认可见度);
    		创建HUD文本(所有玩家(所有队伍), 无, 无, 自定义字符串("说明:由于工坊数字的精度限制、重力与抛出速度的测定误差,落点可能存在少许偏移。"), 左边, 0, 白色, 白色, 白色, 可见和字符串, 默认可见度);
    	}
    }
    
    规则("Event: Set target point")
    {
    	事件
    	{
    		持续 - 每名玩家;
    		双方;
    		全部;
    	}
    
    	条件
    	{
    		按钮被按下(事件玩家, 终极技能) == 真;
    	}
    
    	动作
    	{
    		If(事件玩家.id_target != 0);
    			消除图标(事件玩家.id_target);
    		End;
    		事件玩家.pos_tar = 眼睛位置(事件玩家);
    		创建图标(事件玩家, 所选位置(事件玩家), 箭头:向下, 无, 绿色, 真);
    		事件玩家.id_target = 最后创建的实体;
    		事件玩家.dir_tar_eye = 面朝方向(事件玩家);
    		大字体信息(事件玩家, 自定义字符串("落点已保存"));
    	}
    }
    
    规则("Event: Initialize physical constants")
    {
    	事件
    	{
    		持续 - 全局;
    	}
    
    	动作
    	{
    		"Gravity"
    		全局.g = 10;
    		全局.squg = 全局.g ^ 2;
    		"Sqr of initial speed"
    		全局.squv0 = 30 ^ 2;
    	}
    }
    
    规则("Event: Update direction indicator")
    {
    	事件
    	{
    		持续 - 每名玩家;
    		双方;
    		全部;
    	}
    
    	条件
    	{
    		按钮被按下(事件玩家, 技能1) == 真;
    	}
    
    	动作
    	{
    		If(事件玩家.id_indicator != 0);
    			消除效果(事件玩家.id_indicator);
    		End;
    		If(事件玩家.id_indicator_bottom != 0);
    			消除效果(事件玩家.id_indicator_bottom);
    		End;
    		If(事件玩家.id_time_pred != 0);
    			消除HUD文本(事件玩家.id_time_pred);
    		End;
    		事件玩家.w = 相距距离(矢量(X方向分量(事件玩家.pos_tar), Y方向分量(所选位置(事件玩家)), Z方向分量(事件玩家.pos_tar)), 所选位置(事件玩家));
    		事件玩家.h = Y方向分量(眼睛位置(事件玩家)) - Y方向分量(事件玩家.pos_tar);
    		事件玩家.dir_hor_eye_to_tar = 方向(眼睛位置(事件玩家), 矢量(X方向分量(事件玩家.pos_tar), Y方向分量(眼睛位置(事件玩家)), Z方向分量(事件玩家.pos_tar)));
    		调用子程序(find_direction);
    		调用子程序(pred_time);
    		创建效果(事件玩家, 球体, 红色, 眼睛位置(事件玩家) + 4 * 事件玩家.dir_thrown_at, 0.050, 无);
    		事件玩家.id_indicator = 最后创建的实体;
    		创建HUD文本(事件玩家, 无, 无, 自定义字符串("预计用时:{0}秒", 事件玩家.time_pred), 顶部, 0, 白色, 白色, 红色, 无, 默认可见度);
    		事件玩家.id_time_pred = 上一个文本ID;
    		创建效果(事件玩家, 球体, 橙色, 所选位置(事件玩家), 0.050, 无);
    		事件玩家.id_indicator_bottom = 最后创建的实体;
    		设置朝向(事件玩家, 事件玩家.dir_thrown_at, 至地图);
    		大字体信息(事件玩家, 自定义字符串("指示器已更新"));
    	}
    }
    
    规则("Subroutine: Calculate the direction that grenade is thrown at")
    {
    	事件
    	{
    		子程序;
    		find_direction;
    	}
    
    	动作
    	{
    		事件玩家.squw = 事件玩家.w ^ 2;
    		事件玩家.squh = 事件玩家.h ^ 2;
    		事件玩家.a_div_1000 = 0.004 * (事件玩家.squw + 事件玩家.squh);
    		事件玩家.b_div_10000 = -0.040 * 事件玩家.squw * (事件玩家.h * 全局.g + 全局.squv0) * 0.010;
    		事件玩家.c_div_100000 = 全局.squg * 0.010 * 事件玩家.squw * 0.001 * 事件玩家.squw;
    		事件玩家.delta_div_10000 = 绝对值(事件玩家.b_div_10000) ^ 2 - 4 * 事件玩家.a_div_1000 * 事件玩家.c_div_100000;
    		事件玩家.vhor = 平方根((-1 * 事件玩家.b_div_10000 - 平方根(事件玩家.delta_div_10000) * 1) / (2 * 事件玩家.a_div_1000));
    		事件玩家.vhor = 事件玩家.vhor * 平方根(10);
    		事件玩家.vy = 平方根(全局.squv0 - 事件玩家.vhor ^ 2);
    		事件玩家.v_hor_x = X方向分量(事件玩家.dir_hor_eye_to_tar) * 事件玩家.vhor;
    		事件玩家.v_hor_z = Z方向分量(事件玩家.dir_hor_eye_to_tar) * 事件玩家.vhor;
    		事件玩家.dir_thrown_at = 归一化(矢量(事件玩家.v_hor_x, 事件玩家.vy, 事件玩家.v_hor_z));
    	}
    }
    
    规则("Event: Preview")
    {
    	事件
    	{
    		持续 - 每名玩家;
    		双方;
    		全部;
    	}
    
    	条件
    	{
    		按钮被按下(事件玩家, 互动) == 真;
    		事件玩家.flg_preview == 0;
    	}
    
    	动作
    	{
    		开始镜头(事件玩家, 事件玩家.pos_tar, 事件玩家.pos_tar + 事件玩家.dir_tar_eye, 0);
    		事件玩家.flg_preview = 1;
    	}
    }
    
    规则("Event: Cancel preview")
    {
    	事件
    	{
    		持续 - 每名玩家;
    		双方;
    		全部;
    	}
    
    	条件
    	{
    		事件玩家.flg_preview == 1;
    		按钮被按下(事件玩家, 互动) == 假;
    	}
    
    	动作
    	{
    		事件玩家.flg_preview = 0;
    		停止镜头(事件玩家);
    	}
    }
    
    规则("Subroutine: Calculate the duration")
    {
    	事件
    	{
    		子程序;
    		pred_time;
    	}
    
    	动作
    	{
    		事件玩家.ts = 事件玩家.vy / 全局.g;
    		事件玩家.hs = 0.500 * 全局.g * 事件玩家.ts ^ 2;
    		事件玩家.time_pred = 事件玩家.ts + 平方根(2 * (事件玩家.h + 事件玩家.hs) / 全局.g);
    	}
    }
    
    规则("Event: Teleport")
    {
    	事件
    	{
    		持续 - 每名玩家;
    		双方;
    		全部;
    	}
    
    	条件
    	{
    		按钮被按下(事件玩家, 主要攻击模式) == 真;
    	}
    
    	动作
    	{
    		等待(0.240, 无视条件);
    		传送(事件玩家, 射线命中位置(眼睛位置(事件玩家), 眼睛位置(事件玩家) + 128 * 面朝方向(事件玩家), 所有玩家(所有队伍), 事件玩家, 真) + 射线命中法线(眼睛位置(事件玩家), 眼睛位置(事件玩家) + 128 * 面朝方向(事件玩家),
    			所有玩家(所有队伍), 事件玩家, 真) + 矢量(0, 0, 0));
    	}
    }
    

    英文代码(English Version)

    variables
    {
    	global:
    		0: g
    		1: squv0
    		2: squg
    
    	player:
    		0: w
    		1: h
    		2: a_div_1000
    		3: squw
    		4: squh
    		5: delta_div_10000
    		6: b_div_10000
    		7: vhor
    		8: vy
    		9: v_hor_x
    		10: v_hor_z
    		11: dir_hor_eye_to_tar
    		12: pos_tar
    		13: flg_preview
    		14: dir_thrown_at
    		15: c_div_100000
    		16: id_indicator
    		17: id_target
    		18: dir_tar_eye
    		19: id_time_pred
    		20: time_pred
    		21: ts
    		22: hs
    		23: id_indicator_bottom
    }
    
    subroutines
    {
    	0: find_direction
    	1: pred_time
    }
    
    rule("作者(Author):虐得我喵喵叫#5652")
    {
    	event
    	{
    		Ongoing - Global;
    	}
    
    	actions
    	{
    		Wait(1, Ignore Condition);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("作者:虐得我喵喵叫#5652"), Left, 32, White, White, Orange,
    			Visible To and String, Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("5、按下主要攻击模式键传送。"), Left, 6, White, White, Sky Blue,
    			Visible To and String, Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("4、任意时刻按住互动键进入落点视角。"), Left, 5, White, White, Sky Blue,
    			Visible To and String, Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("3、将对准指示器并抛出手雷,手雷将坠至落点。"), Left, 4, White, White, Sky Blue,
    			Visible To and String, Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("2、按下麻醉镖键唤出/刷新指示器(红色小球)。"), Left, 3, White, White, Sky Blue,
    			Visible To and String, Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("1、按下终极技能键设置当前位置为落点。"), Left, 2, White, White, Sky Blue,
    			Visible To and String, Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("帮助:"), Left, 1, White, White, Sky Blue, Visible To and String,
    			Default Visibility);
    		Create HUD Text(All Players(All Teams), Null, Null, Custom String("说明:由于工坊数字的精度限制、重力与抛出速度的测定误差,落点可能存在少许偏移。"), Left, 0, White,
    			White, White, Visible To and String, Default Visibility);
    	}
    }
    
    rule("Event: Set target point")
    {
    	event
    	{
    		Ongoing - Each Player;
    		All;
    		All;
    	}
    
    	conditions
    	{
    		Is Button Held(Event Player, Ultimate) == True;
    	}
    
    	actions
    	{
    		If(Event Player.id_target != 0);
    			Destroy Icon(Event Player.id_target);
    		End;
    		Event Player.pos_tar = Eye Position(Event Player);
    		Create Icon(Event Player, Position Of(Event Player), Arrow: Down, None, Green, True);
    		Event Player.id_target = Last Created Entity;
    		Event Player.dir_tar_eye = Facing Direction Of(Event Player);
    		Big Message(Event Player, Custom String("落点已保存"));
    	}
    }
    
    rule("Event: Initialize physical constants")
    {
    	event
    	{
    		Ongoing - Global;
    	}
    
    	actions
    	{
    		"Gravity"
    		Global.g = 10;
    		Global.squg = Global.g ^ 2;
    		"Sqr of initial speed"
    		Global.squv0 = 30 ^ 2;
    	}
    }
    
    rule("Event: Update direction indicator")
    {
    	event
    	{
    		Ongoing - Each Player;
    		All;
    		All;
    	}
    
    	conditions
    	{
    		Is Button Held(Event Player, Ability 1) == True;
    	}
    
    	actions
    	{
    		If(Event Player.id_indicator != 0);
    			Destroy Effect(Event Player.id_indicator);
    		End;
    		If(Event Player.id_indicator_bottom != 0);
    			Destroy Effect(Event Player.id_indicator_bottom);
    		End;
    		If(Event Player.id_time_pred != 0);
    			Destroy HUD Text(Event Player.id_time_pred);
    		End;
    		Event Player.w = Distance Between(Vector(X Component Of(Event Player.pos_tar), Y Component Of(Position Of(Event Player)),
    			Z Component Of(Event Player.pos_tar)), Position Of(Event Player));
    		Event Player.h = Y Component Of(Eye Position(Event Player)) - Y Component Of(Event Player.pos_tar);
    		Event Player.dir_hor_eye_to_tar = Direction Towards(Eye Position(Event Player), Vector(X Component Of(Event Player.pos_tar),
    			Y Component Of(Eye Position(Event Player)), Z Component Of(Event Player.pos_tar)));
    		Call Subroutine(find_direction);
    		Call Subroutine(pred_time);
    		Create Effect(Event Player, Sphere, Red, Eye Position(Event Player) + 4 * Event Player.dir_thrown_at, 0.050, None);
    		Event Player.id_indicator = Last Created Entity;
    		Create HUD Text(Event Player, Null, Null, Custom String("预计用时:{0}秒", Event Player.time_pred), Top, 0, White, White, Red, None,
    			Default Visibility);
    		Event Player.id_time_pred = Last Text ID;
    		Create Effect(Event Player, Sphere, Orange, Position Of(Event Player), 0.050, None);
    		Event Player.id_indicator_bottom = Last Created Entity;
    		Set Facing(Event Player, Event Player.dir_thrown_at, To World);
    		Big Message(Event Player, Custom String("指示器已更新"));
    	}
    }
    
    rule("Subroutine: Calculate the direction that grenade is thrown at")
    {
    	event
    	{
    		Subroutine;
    		find_direction;
    	}
    
    	actions
    	{
    		Event Player.squw = Event Player.w ^ 2;
    		Event Player.squh = Event Player.h ^ 2;
    		Event Player.a_div_1000 = 0.004 * (Event Player.squw + Event Player.squh);
    		Event Player.b_div_10000 = -0.040 * Event Player.squw * (Event Player.h * Global.g + Global.squv0) * 0.010;
    		Event Player.c_div_100000 = Global.squg * 0.010 * Event Player.squw * 0.001 * Event Player.squw;
    		Event Player.delta_div_10000 = Absolute Value(Event Player.b_div_10000)
    			^ 2 - 4 * Event Player.a_div_1000 * Event Player.c_div_100000;
    		Event Player.vhor = Square Root((-1 * Event Player.b_div_10000 - Square Root(Event Player.delta_div_10000) * 1) / (
    			2 * Event Player.a_div_1000));
    		Event Player.vhor = Event Player.vhor * Square Root(10);
    		Event Player.vy = Square Root(Global.squv0 - Event Player.vhor ^ 2);
    		Event Player.v_hor_x = X Component Of(Event Player.dir_hor_eye_to_tar) * Event Player.vhor;
    		Event Player.v_hor_z = Z Component Of(Event Player.dir_hor_eye_to_tar) * Event Player.vhor;
    		Event Player.dir_thrown_at = Normalize(Vector(Event Player.v_hor_x, Event Player.vy, Event Player.v_hor_z));
    	}
    }
    
    rule("Event: Preview")
    {
    	event
    	{
    		Ongoing - Each Player;
    		All;
    		All;
    	}
    
    	conditions
    	{
    		Is Button Held(Event Player, Interact) == True;
    		Event Player.flg_preview == 0;
    	}
    
    	actions
    	{
    		Start Camera(Event Player, Event Player.pos_tar, Event Player.pos_tar + Event Player.dir_tar_eye, 0);
    		Event Player.flg_preview = 1;
    	}
    }
    
    rule("Event: Cancel preview")
    {
    	event
    	{
    		Ongoing - Each Player;
    		All;
    		All;
    	}
    
    	conditions
    	{
    		Event Player.flg_preview == 1;
    		Is Button Held(Event Player, Interact) == False;
    	}
    
    	actions
    	{
    		Event Player.flg_preview = 0;
    		Stop Camera(Event Player);
    	}
    }
    
    rule("Subroutine: Calculate the duration")
    {
    	event
    	{
    		Subroutine;
    		pred_time;
    	}
    
    	actions
    	{
    		Event Player.ts = Event Player.vy / Global.g;
    		Event Player.hs = 0.500 * Global.g * Event Player.ts ^ 2;
    		Event Player.time_pred = Event Player.ts + Square Root(2 * (Event Player.h + Event Player.hs) / Global.g);
    	}
    }
    
    rule("Event: Teleport")
    {
    	event
    	{
    		Ongoing - Each Player;
    		All;
    		All;
    	}
    
    	conditions
    	{
    		Is Button Held(Event Player, Primary Fire) == True;
    	}
    
    	actions
    	{
    		Wait(0.240, Ignore Condition);
    		Teleport(Event Player, Ray Cast Hit Position(Eye Position(Event Player), Eye Position(Event Player) + 128 * Facing Direction Of(
    			Event Player), All Players(All Teams), Event Player, True) + Ray Cast Hit Normal(Eye Position(Event Player), Eye Position(
    			Event Player) + 128 * Facing Direction Of(Event Player), All Players(All Teams), Event Player, True) + Vector(0, 0, 0));
    	}
    }
    

    结语

    算作一次简单的尝试吧,非常讨厌不确定因素带来的不安,完成工坊后心情舒畅了许多。希望这个工坊能够对其他的辅助玩家有所帮助。也希望接下来的八月我能够更积极向上一些,你也是。

  • 相关阅读:
    gradle
    1-NIO使用
    处理非正常终止的错误
    一个取消多生产者单消费者的日志线程池服务
    executes()源码
    死锁
    CyclicBarrier使用
    Semaphore
    Spring学习(4)IOC容器配置bean:定义与实例化
    在Maven上Web项目添加Spring框架
  • 原文地址:https://www.cnblogs.com/yuiko/p/ana_parabola.html
Copyright © 2011-2022 走看看