Silverlight 2.5D RPG游戏技巧与特效处理:(二十)副本系统

前段时间晚上把小孩哄睡后带着老婆体验了一把《星辰变》,让我印象较深的可怜只有其副本系统,这里想说并不是《星辰变》的副本有多么有趣;相反,其枯燥到了无生趣可言,几乎你每天都得花费2个小时用在那重复重复再重复,屈指可数那3-5个一成不变的副本任务上,所以没几天我们便厌倦了。自从《魔兽世界》开始侵噬中华网游大地那刻,一款网游“副本系统”设计的好坏往往被商家定位成事关整个游戏品质的极重要环节。为什么地下城模式的副本总能让玩家遐想连篇、无限回味,每一次的进入都能感受新鲜如初?国产网游所谓的副本却永远若脱离不了具有中国特色的任务模式,能不悲哀?

副本系统设计很困难吗?

如果你玩过那些带副本的游戏,相信在你脑海中对于副本这个概念已不陌生。作为策划而言,副本赋予更多的含义是“团队协作”、“独立性”与“探险精神,回报更加丰富而神秘,让人向往。在我看来,游戏副本是基于一个特殊的场景搭建的独立空间,辅以诸多规则、限制与达成、触发条件等,配上计时器统和而成。网游副本,其最初存在的目的是为了弥补多玩家交互而导致的单个/私人领域体验的缺失,即融入更多单机游戏的元素/特性到网游中。如果将网游的主线任务看做是一个故事的线索,那么副本便是游戏的分支剧情,它通常描述着许多精简却非常饱满而完整的故事情节。

话说编写具体副本类实在是太过瘾,用代码书写故事剧本,感觉贼带劲。如前文所述,副本,我们可以看做是特殊的场景空间配上一些UI(阶段描述,任务叙述,倒计时等文字/图形界面)

于是首先第一步还是创建一个基于ObjectBase的副本基类 – InstanceBase

ExpandedBlockStart.gif InstanceBase
     ///   <summary>
    
///  副本类型
    
///   </summary>
     public   enum  InstanceTypes {
        
///   <summary>
        
///  无
        
///   </summary>
        None  =   - 1 ,
        
///   <summary>
        
///  猎杀蜘蛛魔王
        
///   </summary>
        HuntingSpiderKind  =   0 ,
        
///   <summary>
        
///  神邸秘境
        
///   </summary>
        GodFam  =   1 ,
    }

    
public   class  AddRolesEventArgs : EventArgs {
        
public   int  Num {  get set ; }
        
public   int  Mode {  get set ; }
        
public  States State {  get set ; }
        
public  Professions Profession {  get set ; }
        
public  TacticAIs TacticAI {  get set ; }
    }

    
public   class  LeaveEventArgs : EventArgs {
        
public  Teleport Destination {  get set ; }
    }

    
///   <summary>
    
///  副本基类
    
///   </summary>
     public   abstract   class  InstanceBase : ObjectBase {

        
///   <summary>
        
///  添加角色(测试用)
        
///   </summary>
         public   event  EventHandler < AddRolesEventArgs >  AddRoles;

        
///   <summary>
        
///  脱离副本
        
///   </summary>
         public   event  EventHandler < LeaveEventArgs >  Leave;

        
protected  Grid grid  =   new  Grid();
        
protected  TextBlock title  =   new  TextBlock() { Foreground  =   new  SolidColorBrush(Colors.Red), FontSize  =   24  };
        
protected  TextBlock description  =   new  TextBlock() { Foreground  =   new  SolidColorBrush(Colors.White), FontSize  =   22 , TextWrapping  =  TextWrapping.Wrap };
        
protected  TextBlock additionalInformation  =   new  TextBlock() { Foreground  =   new  SolidColorBrush(Colors.Orange), FontSize  =   20  };
        
protected  DispatcherTimer checkTimer  =   new  DispatcherTimer() { Interval  =  TimeSpan.FromMilliseconds( 500 ) };
        
protected  DispatcherTimer countdownTimer  =   new  DispatcherTimer() { Interval  =  TimeSpan.FromSeconds( 1 ) };

        
/// <summary> 副本所属空间 </summary>
         protected  Space space;
        
/// <summary> 参与副本的所有玩家 </summary>
         protected  List < RoleBase >  players;

        
public  InstanceBase() {
            
this .IsHitTestVisible  =   false ;
            RowDefinition row 
=   new  RowDefinition();
            grid.RowDefinitions.Add(row);
            grid.Children.Add(title); Grid.SetRow(title, 
0 ); title.HorizontalAlignment  =  HorizontalAlignment.Center;
            row 
=   new  RowDefinition();
            grid.RowDefinitions.Add(row);
            grid.Children.Add(description); Grid.SetRow(description, 
1 ); description.HorizontalAlignment  =  HorizontalAlignment.Center;
            row 
=   new  RowDefinition();
            grid.RowDefinitions.Add(row);
            grid.Children.Add(additionalInformation); Grid.SetRow(additionalInformation, 
2 ); additionalInformation.HorizontalAlignment  =  HorizontalAlignment.Center;
            
this .Children.Add(grid); Canvas.SetTop(grid,  85 );
            checkTimer.Tick 
+=   new  EventHandler(StepCheck);
            countdownTimer.Tick 
+=   new  EventHandler(Countdown);
            AdaptiveWindowSize();
        }

        
///   <summary>
        
///  获取或设置是否触发各阶段机关
        
///   </summary>
         public   bool [] TriggerOrgan {  get set ; }

        
protected   void  AddRolesEvent(AddRolesEventArgs e) {
            
if  (AddRoles  !=   null ) { AddRoles( this , e); }
        }

        
protected   void  LeaveEvent(LeaveEventArgs e) {
            
if  (Leave  !=   null ) { Leave( this , e); }
        }

        
protected   abstract   void  StepCheck( object  sender, EventArgs e);

        
protected   abstract   void  Countdown( object  sender, EventArgs e);

        
///   <summary>
        
///  触发/运行
        
///   </summary>
         public   abstract   void  Run(List < RoleBase >  players, Space space);

        
///   <summary>
        
///  设置动画描述文本
        
///   </summary>
        
///   <param name="value"></param>
         protected   void  SetDescription( string  value) {
            
if  (description.Text  !=  value) {
                description.Text 
=  value;
                GlobalMethod.RunEffectAnimation(description, 
new  RadialBlur(),  false false " Progress " 0 100 , TimeSpan.FromMilliseconds( 1800 ),  new  ExponentialEase() { EasingMode  =  EasingMode.EaseIn },  true );
            }
        }

        
///   <summary>
        
///  自适应游戏窗口尺寸
        
///   </summary>
         public   virtual   void  AdaptiveWindowSize() {
            grid.Width 
=  Application.Current.Host.Content.ActualWidth;
        }

        
///   <summary>
        
///  离开
        
///   </summary>
         protected   void  Exit( string  titleText) {
            Dispose(
this null );
            title.Text 
=  titleText;
            description.Text 
=   " 5秒后自动离开副本 " ;
            GlobalMethod.RunEffectAnimation(title, 
new  Ripple(),  false false " Progress " 0 100 , TimeSpan.FromMilliseconds( 2000 ),  new  ExponentialEase() { EasingMode  =  EasingMode.EaseOut },  true );
            GlobalMethod.SetTimeout(
delegate  {
                
// 退出
                LeaveEvent( new  LeaveEventArgs() { Destination  =   new  Teleport() { Instance  =  InstanceTypes.None, ToSpace  =   0 , ToDirection  =  Directions.SouthEast, ToCoordinate  =   new  Point( 57 28 ) } });
            }, 
5000 );
        }

    }

以本节Demo源码中的副本-杀蜘蛛魔王】(HuntingSpiderKind.cs)为例,首先是定义其中的阶段(Enum来描述它的故事脉络),这里我用到了中文编码,事实证明了本土语言枚举在副本实现中拥有极高的灵活性,且非常有利于拓展、阅读与维护(前提是还未使用脚本来描述副本):

        ///   <summary> 副本阶段(用本土语言枚举非常有利于拓展维护及阅读) </summary>
        public   enum  Steps {
            设置右键魔法8级连锁闪电或6级石封箭 
=   0 ,
            十二秒内到达传送点 
=   1 ,
            等待4秒翼族来袭 
=   2 ,
            消灭所有翼族 
=   3 ,
            进入传送点到达山的彼岸 
=   4 ,
            设置右键魔法为7级陨石坠落 
=   5 ,
            等待3秒刺客来袭 
=   6 ,
            消灭所有刺客 
=   7 ,
            骑上马并移动到59_60附近开启封印 
=   8 ,
            在23秒内为武器附上烟火粒子 
=   9 ,
            在180秒内消灭蜘蛛魔王 
=   10 ,
            副本完成通过传送门离开 
=   11 ,
        }

        Dictionary
< Steps,  string >  stepExplanation  =   new  Dictionary < Steps,  string > () {
            {Steps.设置右键魔法8级连锁闪电或6级石封箭, 
" 切换到【主角】菜单,将右键魔法设置为8级【连环闪电】或6级【石封箭】 " },
            {Steps.十二秒内到达传送点, 
" 传送点已开启,<紧急>请在12秒内穿过石门找到传送点并进入 " },
            {Steps.等待4秒翼族来袭, 
" 危险!4秒后【翼族】来袭,准备好你的家伙 " },
            {Steps.消灭所有翼族, 
" 用【连环闪电】或【石封箭】干掉他们 " },
            {Steps.进入传送点到达山的彼岸, 
" 通过传送点到达山的彼岸 " },
            {Steps.设置右键魔法为7级陨石坠落, 
" 注意!大规模【守护刺客】将至,将右键魔法设置为7级【陨石坠落】 " },
            {Steps.等待3秒刺客来袭, 
" 危险!3秒后敌军来袭!! " },
            {Steps.消灭所有刺客, 
" 来一杀一,用【陨石坠落】干掉他们 " },
            {Steps.骑上马并移动到59_60附近开启封印, 
" 【骑上马】并移动到坐标【59,60】附近,点击【主角】菜单中的【开启封印】释放【蜘蛛魔王】! " },
            {Steps.在23秒内为武器附上烟火粒子, 
" <紧急>【蜘蛛魔王】23秒后将出现!快速点击【主角】菜单,为【武器】附上【烟火】粒子效果 " },
            {Steps.在180秒内消灭蜘蛛魔王, 
" 3分钟内必须消灭【蜘蛛魔王】,否则世界将被瞬间毁灭!! " },
            {Steps.副本完成通过传送门离开, 
" 副本完成,从传送门离开 " },
        };

副本的故事基于阶段性发展,即完成一个阶段故事才会向下一阶段延续,直到达成该阶段完成条件为止,此时就涉及到副本系统规则的设定、达成判定等处理。我的做法是通过计时器状态机配合倒计时器及角色坐标改变事件来处理所有的逻辑判断,这样的框架性效比极好且能实现任意的副本设计需求:

ExpandedBlockStart.gif 逻辑处理
         public  Steps Step {  get private   set ; }
        Steps countdownStep;
        
int  secondsRemaining;

        
public  HuntingSpiderKind() {
            title.Text 
=   " 副本【猎杀蜘蛛魔王】 " ;
            TriggerOrgan 
=   new   bool [stepExplanation.Count];
        }

        
///   <summary>
        
///  开始
        
///   </summary>
         public   override   void  Run(List < RoleBase >  players, Space space) {
            
this .space  =  space;
            
this .players  =  players;
            players[
0 ].PositionChanged  +=   new  DependencyPropertyChangedEventHandler(player0_PositionChanged);
            checkTimer.Start();
            
// 离开的传送门(测试用)
            space.AddAnimation( new  AnimationBase() {
                ID 
=   0 ,
                Code 
=   81 ,
                Position 
=   new  Point( - 1320 1970 ),
                Z 
=   1970 ,
                Tip 
=   " 离开副本 " ,
                Loop 
=   true ,
            });
            space.RunWeather(WeatherTypes.Cloud, 
50 );
        }

        
/// <summary> 进度检查(网络版中为所有玩家都要检测,目前单机只检测players[0]即主角) </summary>
        protected   override   void  StepCheck( object  sender, EventArgs e) {
            SetDescription(
string .Format( " Step {0}:  {1} " , ( int )Step, stepExplanation[Step]));
            
switch  (Step) {
                
case  Steps.设置右键魔法8级连锁闪电或6级石封箭:
                    
if  ((players[ 0 ].CurrentMagic.Level  ==   8   &&  players[ 0 ].CurrentMagic.Code  ==   9 ||  (players[ 0 ].CurrentMagic.Level  ==   6   &&  players[ 0 ].CurrentMagic.Code  ==   12 )) {
                        space.AddAnimation(
new  AnimationBase() {
                            ID 
=   0 ,
                            Code 
=   82 ,
                            Position 
=   new  Point( - 583 1695 ),
                            Z 
=   1695 ,
                            Tip 
=   " 传送点 " ,
                            Loop 
=   true ,
                        });
                        space.Terrain.AddTeleport(
new  Teleport() {
                            Code 
=   0 ,
                            ToSpace 
=  space.Code,
                            Instance 
=  InstanceTypes.None,
                            ToCoordinate 
=   new  Point( 46 72 ),
                            ToDirection 
=  Directions.SouthEast
                        }, 
" 45_68_0,46_68_0, " );
                        StartCountdown(Steps.十二秒内到达传送点, 
12 );
                    }
                    
break ;
                
case  Steps.消灭所有翼族:
                    
if  (space.AllRoles().Count  ==  players.Count) {  // 这样的检测方案只是暂定的,很可能存在BUG
                        Step  =  Steps.进入传送点到达山的彼岸;
                        space.AddAnimation(
new  AnimationBase() {
                            ID 
=   0 ,
                            Code 
=   82 ,
                            Position 
=   new  Point( - 238 2018 ),
                            Z 
=   2018 ,
                            Tip 
=   " 传送点 " ,
                            Loop 
=   true ,
                        });
                        space.Terrain.AddTeleport(
new  Teleport() {
                            Code 
=   1 ,
                            ToSpace 
=  space.Code,
                            Instance 
=  InstanceTypes.None,
                            ToCoordinate 
=   new  Point( 73 25 ),
                            ToDirection 
=  Directions.SouthWest
                        }, 
" 63_71_0,62_70_0,62_72_0, " );
                    }
                    
break ;
                
case  Steps.设置右键魔法为7级陨石坠落:
                    
if  (players[ 0 ].CurrentMagic.Level  ==   7   &&  players[ 0 ].CurrentMagic.Code  ==   7 ) {
                        StartCountdown(Steps.等待3秒刺客来袭, 
3 );
                    }
                    
break ;
                
case  Steps.消灭所有刺客:
                    
if  (space.AllRoles().Count  ==  players.Count) {
                        Step 
=  Steps.骑上马并移动到59_60附近开启封印;
                    }
                    
break ;
                
case  Steps.骑上马并移动到59_60附近开启封印:
                    
if  (TriggerOrgan[( int )Step]) {
                        StartCountdown(Steps.在23秒内为武器附上烟火粒子, 
23 );
                    }
                    
break ;
                
case  Steps.在23秒内为武器附上烟火粒子:
                    
if  (players[ 0 ].DisplayWeaponParticle) {
                        StartCountdown(Steps.在180秒内消灭蜘蛛魔王, 
180 );
                        Monster boss 
=   new  Monster(space.Terrain) {
                            ID 
=   3120000 ,
                            
// AttachID = 4100000,
                            Code  =   2 ,
                            FullName 
=   " 蜘蛛魔王 " ,
                            LearnedMagic 
=   new  Dictionary < int int > () { {  0 6  }, {  1 6  }, {  2 6  }, {  3 6  }, {  4 6  }, {  5 6  }, {  6 6  }, {  7 6  }, {  8 6  }, {  9 6  }, {  10 6  }, {  11 6  }, {  12 6  }, {  13 6  }, {  14 6  }, {  15 6  }, {  16 6  }, {  17 6  }, {  18 6  } },
                            Profession 
=  Professions.Monster,
                            ArmorCode 
=   16 ,
                            Direction 
=  Directions.SouthEast,
                            State 
=  States.Walking,
                            Camp 
=  Camps.Eval,
                            TacticAI 
=  TacticAIs.GoalLeader,
                            ActionAI 
=  ActionAIs.Persistent,
                            LifeMax 
=   640000 ,
                            Life 
=   640000 ,
                            ATK 
=   24059 ,
                            DEF 
=   1500 ,
                            MAG 
=   500 ,
                            DEX 
=  ObjectBase.RandomSeed.Next( 0 30 ),
                            Coordinate 
=   new  Point( 49 59 )
                        };
                        space.AddRole(boss, 
new  RoleAddedEventArgs() {
                            RegisterDisposedEvent 
=   true ,
                            RegisterIntervalTriggerEvent 
=   true ,
                            RegisterActionTriggerEvent 
=   true ,
                            RegisterDoAttackEvent 
=   true ,
                            RegisterDoCastingEvent 
=   true ,
                            RegisterPositionChangedEvent 
=   true ,
                            RegisterLifeChangedEvent 
=   true ,
                        });
                        space.MusicUri 
=   " Boss " ;
                    }
                    
break ;
                
case  Steps.在180秒内消灭蜘蛛魔王:
                    
if  (space.AllRoles().Count  ==  players.Count) {
                        Step 
=  Steps.副本完成通过传送门离开;
                        space.MusicUri 
=   " 100 " ;
                        space.AddAnimation(
new  AnimationBase() {
                            ID 
=   0 ,
                            Code 
=   81 ,
                            Position 
=   new  Point( - 300 1591 ),
                            Z 
=   1591 ,
                            Tip 
=   " 传送到【废墟秘境】 " ,
                            Loop 
=   true ,
                        });
                    }
                    
break ;
            }
        }

       
///   <summary>
       
///  开始倒计时
       
///   </summary>
        void  StartCountdown(Steps step,  int  remaining) {
           countdownStep 
=  Step  =  step;
           secondsRemaining 
=  remaining;
           countdownTimer.Start();
       }

        
///   <summary>
        
///  倒计时
        
///   </summary>
         protected   override   void  Countdown( object  sender, EventArgs e) {
            
if  (Step  !=  countdownStep) {
                countdownTimer.Stop();
                additionalInformation.Text 
=   "" ;
            } 
else  {
                additionalInformation.Text 
=   string .Format( " 倒计时: {0} " , secondsRemaining);
                
if  (secondsRemaining  ==   0 ) {
                    additionalInformation.Text 
=   "" ;
                    
switch  (Step) {
                        
case  Steps.十二秒内到达传送点:
                        
case  Steps.在23秒内为武器附上烟火粒子:
                        
case  Steps.在180秒内消灭蜘蛛魔王:
                            Exit(
" 副本【猎杀蜘蛛魔王】  战斗失败 " );
                            
break ;
                        
case  Steps.等待4秒翼族来袭:
                            Step 
=  Steps.消灭所有翼族;
                            AddRolesEvent(
new  AddRolesEventArgs() { Num  =   10 , Mode  =   1 , Profession  =  Professions.Archer, State  =  States.Riding, TacticAI  =  TacticAIs.GoalLeader });
                            
break ;
                        
case  Steps.等待3秒刺客来袭:
                            Step 
=  Steps.消灭所有刺客;
                            AddRolesEvent(
new  AddRolesEventArgs() { Num  =   30 , Mode  =   2 , Profession  =  Professions.Assassin, State  =  States.Walking, TacticAI  =  TacticAIs.GoalLeader });
                            
break ;
                    }
                }
                secondsRemaining
-- ;
            }
        }

        
void  player0_PositionChanged( object  sender, DependencyPropertyChangedEventArgs e) {
            Point p 
=  (Point)(e.NewValue);
            
int  x  =  ( int )p.X;
            
int  y  =  ( int )p.Y;
            
if  ((x  ==   41   &&  y  ==   90 ||  (x  ==   40   &&  y  ==   91 )) {
                LeaveEvent(
new  LeaveEventArgs() { Destination  =   new  Teleport() { Instance  =  InstanceTypes.None, ToSpace  =   0 , ToDirection  =  Directions.SouthEast, ToCoordinate  =   new  Point( 57 28 ) } });
                Dispose(
this null );
            } 
else   if  (Step  ==  Steps.十二秒内到达传送点  &&  x  ==   46   &&  y  ==   72 ) {
                StartCountdown(Steps.等待4秒翼族来袭, 
4 );
            } 
else   if  (Step  ==  Steps.进入传送点到达山的彼岸  &&  x  ==   73   &&  y  ==   25 ) {
                Step 
=  Steps.设置右键魔法为7级陨石坠落;
            } 
else   if  (Step  ==  Steps.副本完成通过传送门离开  &&  ((x  ==   48   &&  y  ==   59 ||  (x  ==   48   &&  y  ==   58 ||  (x  ==   47   &&  y  ==   58 ))) {
                LeaveEvent(
new  LeaveEventArgs() { Destination  =   new  Teleport() { Instance  =  InstanceTypes.GodFam, ToSpace  =   101 , ToDirection  =  Directions.NorthWest, ToCoordinate  =   new  Point( 60 44 ) } });
                Dispose(
this null );
            }
        }

本节源码中我为大家提供了两个副本做为参考:【猎杀蜘蛛魔王】和【神邸秘境】,它们可以独立进行,同时当杀掉【猎杀蜘蛛魔王】副本中的BOSS后,通过传送门同样可以进入到下一副本【神邸秘境】,这样就构成了副本系统中的连锁副本设计,从而形成无线连通的副本世界,能够满足所有一切的副本设计需求。另外每个副本都包含有10几个阶段,比如让玩家改变属性、移动到指定坐标、进入传送点、杀光怪、倒计时行为、开启封印(使用道具,物品道具都应有相关属性来记录它的各类效果或触发事件)、杀掉BOSS、定时刷怪、跟随移动、对话、守护等等,基于本节副本系统框架制作的副本只有想不到的,没有做不到的(功能实现暂时是硬编码,以后有机会再进一步完善,细节/方法还可以再优化)

做为商业游戏开发,副本系统可以基于脚本构建,同时副本编辑器也必不可少,设计起来并不难,关键点本文已讲述得很清楚了。本文,我以副本功能的实现让大家对副本系统的设计有个入门概念,如何将其制定成像场景一样可动态编辑的对象,这个需要预先对游戏产品中所有副本可能需要拥有最极限的功能需求进行分析,然后在制作好副本编辑器后配以脚本和xml描述文件最终实现最完美的副本系统。另外,如果你体验过本节的Demo相信应该还会有这样的领悟:这副本咋和新手指导(入门演示)如此相似?没错,其实它们本就同根同源,想想原理便一清二楚了。

副本的最后一个要素便是场景的渲染和背景音乐烘托,在云雾缭绕的秘境中伴随着激荡人心的音乐与BOSS华丽一战,相信那样的体验足够你珍惜与回味。

深思呀,国产网游;副本不仅仅代表着财宝与经验,韵味深长的用户体验才是至尊之道。

本系列源码请到目录中下载

在线演示地址:http://cangod.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值