程序层面,有效提升打击感的方法:
- 卡帧
- 震屏
- 精准的特效和音效
- 区分轻重
- 需要卡帧时直接去改Time.timeScale
- 提供一个曲线来设置timeScale的渐变效果
- 缺点:可能让玩家感觉游戏很卡,不够流畅
- 每个角色有自己的LocalTimeScale,会叠加上全局的TimeScale
- 影响自己身上的状态机,动画和特效
- 有localTimeScale的情况下,globalTimeScale一般是1,除非暂停.
- 玩家打到Boss时,Boss卡顿一下而玩家几乎不卡,这样操作感依然很流畅
- 容易实现子弹时间
可以很方便地用来做子弹时间,别人很慢而自己很快,塞尔达里那种时快时慢的子弹时间也可以. 扩展以后还可以支持时间回溯 (参考Braid),需要用到帧同步或者状态同步.
- 动画系统 –> Animator speed
- 粒子系统 -> Simulate 或者 simulationSpeed
- 需要提供Yield return waitForLocalSeconds方法,延时操作都需要用这个
- 当多个操作同时改变TimeScale的时候,需要根据优先级决定采用哪个timeScale.
public interface ITimeZone {
float relativeTimeScale { get; set; }
float timeScale {get; set;}
float deltaTime {get;}
float fixedDeltaTime {get;}
// we need to handle multiple sources of timeScale change.
void ChangeTimeScale(float newTimeScale, int priority);
// Sometimes we need to tween timeScale, such as bullet time.
// Same as above, we need to handle multiple sources by priority.
void TweenTimeScale(ITweenInfo tweenInfo, int priority);
}
void DoUpdate(){
hero.animator.speed = hero.localTimeScale;
}
通用方案下,每个粒子系统的TimeScale需要单独调整
IEnumerator PlayEffectCrt(float duration){
if(hero.localTimeScale == 1)
yield break;
float timer;
while(timer < duration){
particleSystem.Simulate(hero.localDeltaTime);
timer += hero.localDeltaTime;
yield return null;
}
}
注意: Simulate()方法会产生大量GC.
particleSystem.main.simulationSpeed = hero.localTimeScale;
某些特效可能不受TimeScale影响,始终按照正常速度播放
if(effect.playInUnscaledTime)
particleSystem.main.simulationSpeed = 1;
IEnumerator WaitForSecondsLocal(float duration) {
float timer = 0;
while(timer <= duration) {
timer += LocalDeltaTime;
yield return null;
}
}
// Example
IEnumerator DoSkill(Skill skill){
SkillOn(skill);
yield return hero.WaitForSecondsLocal(skill.duration);
SkillEnd();
}
区分轻重硬直和动画,打击感才会丰富,后面会详细讲. 硬直动画用状态机来实现
主要是摄像机晃动的效果,表现地面震动
推荐插件: https://github.com/ewersp/CameraShake
- 多个震屏之间相互独立,不干扰
- 平移和转动
- 多种震动曲线
- 使用频次太高会感觉头晕
音效和特效都要卡在合适的动画帧播放
- 策划通过表格配置延迟时间,需要找到动画帧数,并换算成延迟
- 技能分为前摇,激活,后摇三个阶段,方便生成判定和特效
- 容易上手但修改和预览很麻烦,尤其是动画有频繁改动的时候
- 用Animation Event 控制特效和声音播放
- 省去了换算时间和配表的步骤.
- 还是没有解决预览的问题, 特效配置可能不容易集中管理
- 适合Demo快速迭代,如果用于大项目需要自己写插件,管理所有的AnimationEvent
API参考:
http://gad.qq.com/article/detail/38140
最好制作一个方便预览的场景,但很难满足特效和策划的要求。设计代码的时候就要考虑预览问题.
- 对于动作游戏,只传入一个数字的伤害肯定是不够的。
- 封装成Damage类, 记录释放者,Skill, Buff, 气绝伤害,破防,硬直类型,物理受力等
- 根据技能硬直类型和受伤者的硬直抵抗来决定最后的状态
- 参考Dota2的伤害日志. 应该做到这样的详细程度
- 播放不同动画,持续不同时间,是否无敌
- 多阶段的动画, 有特殊运动,比如浮空后倒地,或者投技
- 状态机要支持Coroutine,这样分阶段的时候方便很多
轻量状态机,带调试功能:
https://github.com/ImYellowFish/Unity3d-Finite-State-Machine
- 玩家原始输入->中间处理器->最终输入->状态机。各个中间处理器需要协调好彼此关系.
如果可以构成连续技,实际触发的Skill可能并不是Input中指定的Skill 比如普攻第二段是上挑,第三段下劈, 等等.
Input传入攻击的时候,如果该技能不能立刻释放(正在放其他技能,或者硬直),则等到玩家就绪 的时候释放.
- 基本实现是一个深度只有1的指令栈,处理优先级高于连续技.
- 当玩家进入就绪状态后自动触发.
- 需要状态机提供查询接口(FireEvent需要传回一个bool而不是void)
Input传入攻击的时候,如果玩家就绪,但是距离要求不满足,则走进范围后再释放. 也是一个中间处理器. 以上三个处理器的耦合需要小心处理
比如:A是否能取消B前摇,A能否取消B激活,A能否取消B后摇
-
判断下是否已经在释放技能,如果是,则判断取消规则.
-
具体用什么规则看战斗系统的设计 比如每个技能都有int优先级,优先级高的可以取消优先级低的前摇.
-
不要让策划拍脑子想,程序制定的规则肯定比策划靠谱
- 构成连续技的可以取消前一技能的后摇
- 某些技能可以取消大部分技能的任何阶段(比如闪避)
- 某些技能可以取消某些硬直状态(比如受身取消倒地)
动作游戏的技能经常会用到. 某些技能会带有特殊属性,比如破防,强制取消,带格挡,等等. 在读表系统中加入对Flag Enum类型的支持,可以减少很多列
Flag规则可以参考2D格斗游戏:
http://wiki.shoryuken.com/Skullgirls/Glossary
- 无视boss霸体但只造成轻受击=interrupt
- 无视boss霸体=ignore_guard
- 格挡=block
- 格挡反击=counter
- 霸体=armor
- 无视体积碰撞=no_collision
- 回避可以在动作的任何时间点取消=evade_cancel
- 所有动作都无法取消==no_cancel
- 跑步取消后摇=run_cancel
https://github.com/ImYellowFish/UnityUtility/tree/master/ModedStandardV2017
- 只考虑Forward rendering的Fragment shader.
- 原版StandardShader已经计算出了所需的LightDir, Normal, ViewDir等变量.
- 用ShaderFeature关键字生成多个shader
- 自定义Editor用来开关各个Feature
// file: UnityStandardCore.cginc
half4 fragForwardBaseInternal (VertexOutputForwardBase i)
{
FRAG_SETUP(i);
// Unity standard PBS
half4 c = UNITY_BRDF_PBS (s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, gi.light, gi.indirect);
//-----------custom functions (forward base)-------------
// apply custom Emission
c.rgb += Emission(i.tex.xy) * _EmissionMultiplier;
#ifdef _ZGAME_HSBC
// apply HSBC effect
c.rgb = HSBC(c.rgb, _HsbcParam, i.tex.xy);
#endif
#ifdef _ZGAME_RIM
// apply rim color
c.rgb += Rim(s);
#endif
// apply extra light
#ifdef _ZGAME_FORWARD_BASE_MULTI_LIGHT
c.rgb += ExtraLight(s, _ExtraLightDir_1, _ExtraLightColor_1, ExtraLightAtten(s.posWorld, _ExtraLightPos_1, _ExtraLightDir_1));
c.rgb += ExtraLight(s, _ExtraLightDir_2, _ExtraLightColor_2, ExtraLightAtten(s.posWorld, _ExtraLightPos_2, _ExtraLightDir_2));
#endif
#ifdef _ZGAME_SUBSURFACE_SCATTERING
// apply subsurface scattering
c.rgb = c.rgb + SSS(mainLight.dir, s.normalWorld, -s.eyeVec, mainLight.color, i.tex.xy);
#endif
#ifdef _ZGAME_UV_FLOW
// apply uv flow
c.rgb += UV_Flow(i.tex);
#endif
//------------end custom functions --------
UNITY_APPLY_FOG(i.fogCoord, c.rgb);
return OutputForward (c, s.alpha);
}
- 给单个物体加两个单独的虚拟方向光
- 在ForwardBase pass中,除了主光源还额外计算两个光照
- 好处:美术调节更自由.
- 节省Draw call,不用设置Layer.
- 防止多个灯光破坏Batching.
#ifdef _ZGAME_FORWARD_BASE_MULTI_LIGHT
half4 _ExtraLightDir_1;
fixed4 _ExtraLightColor_1;
fixed4 _ExtraLightPos_1;
half4 _ExtraLightDir_2;
fixed4 _ExtraLightColor_2;
fixed4 _ExtraLightPos_2;
inline fixed3 ExtraLight(FragmentCommonData s, half4 lightDir, fixed4 lightColor, fixed atten)
{
UnityLight l;
l.color = lightColor.rgb * lightColor.a * 2 * atten;
l.dir = lightDir.xyz;
UnityIndirect noIndirect = ZeroIndirect();
return UNITY_BRDF_PBS(s.diffColor, s.specColor, s.oneMinusReflectivity, s.smoothness, s.normalWorld, -s.eyeVec, l, noIndirect);
}
inline half ExtraLightAtten(half3 worldPos, half4 lightPos, half4 lightDir) {
half dist_proj = dot(worldPos - lightPos.xyz, lightDir.xyz);
half dist2 = dist_proj * dist_proj;
return saturate((lightPos.w + 1) / (dist2 + 0.01) - lightPos.w);
}
#endif
- 用Ramp Texture修正光照,取样参数是dot(normal, lightDir)
- 在明暗交界处加上淡红色
- 支持Mask,过滤掉头发和衣服等部位
- 低消耗的SSS效果,作为补光来说够用了
#ifdef _ZGAME_SUBSURFACE_SCATTERING
half _SSS_Sigma;
half _SSS_Power;
fixed4 _SSS_Color;
sampler2D _SSS_Ramp;
sampler2D _SSS_Mask;
// subsurface scattering
inline fixed3 SSS(half3 lightDir, half3 normal, half3 viewDir, fixed3 lightColor, half2 uv) {
// half f = _SSS_Sigma * max(dot(-normal, lightDir), 0) + (1 - _SSS_Sigma) * (dot(-viewDir, lightDir) * 0.5 + 0.5);
// fixed3 c = pow(f, _SSS_Power) * _SSS_Color.rgb * 2;
half nl = dot(normal, lightDir);
fixed3 c = pow(tex2D(_SSS_Ramp, half2(nl / 2 + 0.5, 0.5)), _SSS_Power) * _SSS_Color.rgb * lightColor;
// apply mask
c = c * tex2D(_SSS_Mask, uv).r;
return c;
}
#endif
边缘光, 用来制作受击的闪光效果, 比描边算法要省一点.
#ifdef _ZGAME_RIM
fixed3 _RimColor;
half _RimPower;
// Add rim color to this character
inline fixed3 Rim(FragmentCommonData s) {
half rimDot = 1 - saturate(dot(s.normalWorld, -s.eyeVec));
return _RimColor * pow(rimDot, _RimPower);
}
#endif
改变色调,饱和度,亮度和对比度, 用来制作变身后的色调变化
https://forum.unity3d.com/threads/hue-saturation-brightness-contrast-shader.260649/
根据美术需求做一些Mask blending.