我正在使用C#,但这适用于开发任何语言的游戏。
大多数游戏使用“游戏循环”,看起来像这样:
while (game is running)
{
UpdateEnvironment();
DrawEnvironment();
}
我正在努力理解如何将不止一个游戏循环的东西融入等式中。例如,使能量球从一个瓷砖浮动到另一个瓷砖。或者让玩家移动一个图块(不是从图块跳到图块而是动画到它)。
我提出的最好的事情是花费自上一次循环以来经过的时间,并将其传递给对象/方法,以便它可以做到这一点。但这使得很难做到这样的事情:
AI.MoveTo(10, 20); // Pathfind, then walk the path to this tile.
Player.Shoot(); // Shoot a bullet, and detect collisions and update along the way.
在哪里可以找到有关“执行需要多个游戏循环才能完成的事件”的更多信息?
答案 0 :(得分:1)
一种方法是存储完整的待处理操作,然后让游戏循环只执行一点点操作。从动作引用的游戏对象知道它们所处的状态,因此下一个要执行的位是已知的。虽然还有一些事情需要做,但操作会被添加回要在下一个循环中执行的待处理操作的队列,并且当操作完成后,它将不再被添加回来。
因此,在您的MoveTo
示例中,要存储的操作是移动到10, 20
,并且每次在整个游戏循环中,AI
都会向它移动一点点。你的Shoot
示例可能更好地描述为朝着某个方向行进的子弹,然后无论它击中什么都决定了行动是否继续。
我还没有完成游戏开发,所以我不知道这是否是在该领域中完成的,但这是我在基于事件的系统中做这样的事情。
答案 1 :(得分:1)
实际上,你所说的关于使用过去的内容是相当准确的。如果您熟悉C#,并且还没有这样做,我强烈建议您研究一下XNA,或者如果您愿意花一点钱,可以通过“TheGameCreators”来购买Dark GDK .NET。
无论如何,我们的想法是,对于每个游戏循环,您使用自上次更新以来经过的时间更新任何“实时”对象。 “实时”物体是任何仍然被认为是活跃的并且需要更新的物体(例如,在飞行途中的敌人,玩家,子弹,仍在爆炸的爆炸等)。您可以根据经过的时间,碰撞,火灾造成的损坏等确定每个对象应具有的下一个状态,位置,运行状况等,然后实现下一个状态。最后,然后为每个对象调用绘制过程,并以新状态呈现它们。
对于类似玩家射击的事情,这是你可以做的事情。注意,这是比任何更多的伪代码。希望它能给你一个想法。
//Generic Game Class
public class MySweetGame : Game
{
Player _player = new Player(this);
List<Bullet> _bullets = new List<Bullet>();
public List<Bullet> AllBullets()
{
get { return _bullets; }
}
public void Update()
{
//You would obviously need some keyboard/mouse class to determine if a click took place
if(leftMouseButtonClicked)
{
_player.Shoot();
}
//This would be assuming you have an object collection or array to hold all the bullets currently 'live' like the above '_bullets' collection
//This is also assuming elapseGameTime is some built in game time management, like in XNA
foreach(Bullet blt in _bullets)
{
blt.Update(elapsedGameTime);
}
}
}
//Generic Player Class
public class Player()
{
Vector2 _position = new Vector2(0,0);
int _ammunition = 50;
MySweetGame _game;
public Player(MySweetGame game)
{
_game = game;
}
public void Shoot()
{
if(_ammunition > 0){
_game.AllBullets.Add(new Bullet(50, _position));
_ammunition--;
}
}
}
//Generic Bullet Class
public class Bullet()
{
float _metersPerSecond = 0;
Vector2 _position = new Vector2(0, 0);
public Bullet(float metersPerSecond, Vector3 position)
{
_metersPerSecond = metersPerSecond;
_position = position;
}
//Here is the meat you wanted
//We know the speed of the bullet, based on metersPerSecond - which we set when we instantiated this object
//We also know the elapsedGameTime since we last called update
//So if only .25 seconds have passed - we only moved (50 * .25) - and then update our position vector
public void Update(float elapsedGameTime)
{
distanceTraveled = metersPerSecond * elapsedGameTime;
_position.x += distanceTraveled;
}
}
答案 2 :(得分:1)
你可能不会使用事件;相反,MoveTo或Shoot应被视为状态的变化。您的AI对象需要一个由以下变量组成的状态:
class AI
{
StateEnum State; //Idle, Moving, Attacking, Dying, etc.
PointF Location;
PointF Velocity;
PointF Destination;
在MoveTo方法中,您需要设置对象的状态 - 例如:
void MoveTo(x, y)
{
Destination = new PointF(x, y);
Velocity = new PointF(0.5, 0);
State = StateEnum.Moving;
}
然后在其Update方法中,您将更新位置。
void Update()
{
switch (State)
{
case StateEnum.Moving:
Location.Offset(Velocity); //advance 0.5 pixels to the right
break;
default:
break;
}
}
}
该方法将基于某个计时器(例如每秒60个刻度)从游戏循环调用,因此实际上,该对象每秒移动30个像素。如果它有动画帧,只需用滴答计数倒计时并根据需要更改帧。
对于寻路,要从平铺移动到平铺,您可以更新每个平铺的速度,以便对象沿所需方向移动。
答案 3 :(得分:1)
除了WesleyJohnson和Gannon的解决方案之外,您还可以使用基于任务的方法。 WesleyJohnson和Gannon的解决方案具有较低的复杂性,这是一件好事,尤其是当您的游戏角色的行为被静态定义时。喜欢简单的射击游戏。但是当您希望通过脚本动态定义行为时,或者当您的actor具有复杂行为时,您可能希望将行为管理外部化。 因为你的演员的更新功能必须具有复杂的状态管理。
一种常见的方法是拥有一个名为Task(或Process或Job)的基类 并且特定的更长时间运行的任务子类Job。例如,您可以拥有MoveActorTask,PlayAnimationTask等。通过结果代码和标记它们是否已完成,您还可以以一种方式链接任务,一次一个地执行,等待使用{{3}完成前者}
以下是我们使用的内容,稍加编辑以便更好地阅读,并从一些可能会引起混淆的高级选项中删除:
class Task
{
public:
/**
* Constructor.
*
* @param isDiscardable Set this true, if the Task's goal can be reached in a single step.
* For instance if a Task is supposed to slowly close a window by fading
* its alpha to 0, then it is discardable, and Task#discard will just finish
* the process by closing the window.
*
* @param destroyWhenDone Set this to true, when the TaskScheduler shall delete the
* Task, after execution is finished. This should usually be the case, but
* sometimes it is sensible to pool a number of Jobs for reuse.
*/
Task(bool isDiscardable,
bool destroyWhenDone);
virtual ~Task();
/**
* This is the function in which the Task is supposed to do whatever it is supposed to do.
* This function is called by the TaskScheduler at most once per frame. The frequency depends
* on the Job's priority given with TaskScheduler#addTask.
* @param time the time source time, since the last call of this function.
* @return true, when the Task is done, false else. If false is returned, the Task will be
* rescheduled for another execution.
*/
virtual bool execute(Time time) = 0;
virtual TimeSource::TimeSourceType getTimeSource() const = 0;
/// Returns whether the Task can be removed from the queue by the scheduler,
bool isDiscardable() const;
/// Returns true, if the Task shall be deleted, if the Job is finished. Returns false else.
bool destroyWhenDone() const;
/// Finish whatever the Task is doing. It won't get a chance to continue.
/// Overloaded functions must *not* call this implementation.
virtual void discard();
protected:
bool mIsDiscardable;
bool mDestroyWhenDone;
};
任务由TaskScheduler管理。 TaskScheduler为每个任务(composite tasks)调用任务的execute
函数的每一帧,或者您可以有不同的调度策略。
答案 4 :(得分:-2)
考虑操作系统如何允许多个程序在单个处理器上运行:
这种“中断/保存/恢复/恢复”方法对于真正难以分解成部分的任务来说是一个“最坏情况”选项。在某一时刻(可能基于任务运行的时间),您可以保存所有任务所需的变量,并停止运行代码。稍后,您可以恢复状态并恢复代码。
然而,通常可以设计您的系统,以减少诉诸此类事件的需要。例如,设计动画以便可以一次处理一帧。