Package horizons :: Package world :: Package units :: Module movingobject
[hide private]
[frames] | no frames]

Source Code for Module horizons.world.units.movingobject

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import logging 
 23  from typing import TYPE_CHECKING, Type 
 24   
 25  from fife import fife 
 26   
 27  from horizons.component.componentholder import ComponentHolder 
 28  from horizons.constants import GAME_SPEED 
 29  from horizons.scheduler import Scheduler 
 30  from horizons.util.pathfinding import PathBlockedError 
 31  from horizons.util.python.weakmethodlist import WeakMethodList 
 32  from horizons.util.shapes import Point 
 33  from horizons.world.concreteobject import ConcreteObject 
 34  from horizons.world.units import UnitClass 
 35  from horizons.world.units.unitexeptions import MoveNotPossible 
 36   
 37  if TYPE_CHECKING: 
 38          from horizons.util.pathfinding.pather import AbstractPather 
 39   
 40   
41 -class MovingObject(ComponentHolder, ConcreteObject):
42 """This class provides moving functionality and is to be inherited by Unit. 43 Its purpose is to provide a cleaner division of the code. 44 45 It provides: 46 *attributes: 47 - position, last_position: Point 48 - path: Pather 49 50 *moving methods: 51 - move 52 - stop 53 - add_move_callback 54 55 *getters/checkers: 56 - check_move 57 - get_move_target 58 - is_moving 59 """ 60 movable = True 61 62 log = logging.getLogger("world.units") 63 64 # overwrite this with a descendant of AbstractPather 65 pather_class = None # type: Type[AbstractPather] 66
67 - def __init__(self, x, y, **kwargs):
68 super().__init__(x=x, y=y, **kwargs) 69 self.__init(x, y)
70
71 - def __init(self, x, y):
72 self.position = Point(x, y) 73 self.last_position = Point(x, y) 74 self._next_target = Point(x, y) 75 76 self.move_callbacks = WeakMethodList() 77 self.blocked_callbacks = WeakMethodList() 78 self._conditional_callbacks = {} 79 80 self.__is_moving = False 81 82 self.path = self.pather_class(self, session=self.session) 83 84 self._exact_model_coords1 = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose) 85 self._exact_model_coords2 = fife.ExactModelCoordinate() # save instance since construction is expensive (no other purpose) 86 self._fife_location1 = None 87 self._fife_location2 = None
88
89 - def check_move(self, destination):
90 """Tries to find a path to destination 91 @param destination: destination supported by pathfinding 92 @return: object that can be used in boolean expressions (the path in case there is one) 93 """ 94 return self.path.calc_path(destination, check_only=True)
95
96 - def is_moving(self):
97 """Returns whether unit is currently moving""" 98 return self.__is_moving
99
100 - def stop(self, callback=None):
101 """Stops a unit with currently no possibility to continue the movement. 102 The unit actually stops moving when current move (to the next coord) is finished. 103 @param callback: a parameter supported by WeakMethodList. is executed immediately if unit isn't moving 104 """ 105 if not self.is_moving(): 106 WeakMethodList(callback).execute() 107 return 108 self.move_callbacks = WeakMethodList(callback) 109 self.path.end_move()
110
111 - def _setup_move(self, action='move'):
112 """Executes necessary steps to begin a movement. Currently only the action is set.""" 113 # try a number of actions and use first existent one 114 for action_iter in (action, 'move', self._action): 115 if self.has_action(action_iter): 116 self._move_action = action_iter 117 return 118 # this case shouldn't happen, but no other action might be available (e.g. ships) 119 self._move_action = 'idle'
120
121 - def move(self, destination, callback=None, destination_in_building=False, action='move', 122 blocked_callback=None, path=None):
123 """Moves unit to destination 124 @param destination: Point or Rect 125 @param callback: a parameter supported by WeakMethodList. Gets called when unit arrives. 126 @param action: action as string to use for movement 127 @param blocked_callback: a parameter supported by WeakMethodList. Gets called when unit gets blocked. 128 @param path: a precalculated path (return value of FindPath()()) 129 """ 130 if not path: 131 # calculate the path 132 move_possible = self.path.calc_path(destination, destination_in_building) 133 134 self.log.debug("%s: move to %s; possible: %s; is_moving: %s", self, 135 destination, move_possible, self.is_moving()) 136 137 if not move_possible: 138 raise MoveNotPossible 139 else: 140 self.path.move_on_path(path, destination_in_building=destination_in_building) 141 142 self.move_callbacks = WeakMethodList(callback) 143 self.blocked_callbacks = WeakMethodList(blocked_callback) 144 self._conditional_callbacks = {} 145 self._setup_move(action) 146 147 # start moving by regular ticking (only if next tick isn't scheduled) 148 if not self.is_moving(): 149 self.__is_moving = True 150 # start moving in 1 tick 151 # this assures that a movement takes at least 1 tick, which is sometimes subtly 152 # assumed e.g. in the collector code 153 Scheduler().add_new_object(self._move_tick, self)
154
155 - def _movement_finished(self):
156 self.log.debug("%s: movement finished. calling callbacks %s", self, self.move_callbacks) 157 self._next_target = self.position 158 self.__is_moving = False 159 self.move_callbacks.execute()
160
161 - def _move_tick(self, resume=False):
162 """Called by the scheduler, moves the unit one step for this tick. 163 """ 164 assert self._next_target is not None 165 166 if self._fife_location1 is None: 167 # this data structure is needed multiple times, only create once 168 self._fife_location1 = fife.Location(self._instance.getLocationRef().getLayer()) 169 self._fife_location2 = fife.Location(self._instance.getLocationRef().getLayer()) 170 171 if resume: 172 self.__is_moving = True 173 else: 174 #self.log.debug("%s move tick from %s to %s", self, self.last_position, self._next_target) 175 self.last_position = self.position 176 self.position = self._next_target 177 self._changed() 178 179 # try to get next step, handle a blocked path 180 while self._next_target == self.position: 181 try: 182 self._next_target = self.path.get_next_step() 183 except PathBlockedError: 184 # if we are trying to resume and it isn't possible then we need to raise it again 185 if resume: 186 raise 187 188 self.log.debug("path is blocked") 189 self.log.debug("owner: %s", self.owner) 190 self.__is_moving = False 191 self._next_target = self.position 192 if self.blocked_callbacks: 193 self.log.debug('PATH FOR UNIT %s is blocked. Calling blocked_callback', self) 194 self.blocked_callbacks.execute() 195 else: 196 # generic solution: retry in 2 secs 197 self.log.debug('PATH FOR UNIT %s is blocked. Retry in 2 secs', self) 198 # technically, the ship doesn't move, but it is in the process of moving, 199 # as it will continue soon in general. Needed in border cases for add_move_callback 200 self.__is_moving = True 201 Scheduler().add_new_object(self._move_tick, self, 202 GAME_SPEED.TICKS_PER_SECOND * 2) 203 self.log.debug("Unit %s: path is blocked, no way around", self) 204 return 205 206 if self._next_target is None: 207 self._movement_finished() 208 return 209 else: 210 self.__is_moving = True 211 212 # HACK: 2 different pathfinding systems are being used here and they 213 # might not always match. 214 # If the graphical location is too far away from the actual location, 215 # then forcefully synchronize the locations. 216 # This fixes the symptoms from issue #2859 217 # https://github.com/unknown-horizons/unknown-horizons/issues/2859 218 pos = fife.ExactModelCoordinate(self.position.x, self.position.y, 0) 219 fpos = self.fife_instance.getLocationRef().getExactLayerCoordinates() 220 if (pos - fpos).length() > 1.5: 221 self.fife_instance.getLocationRef().setExactLayerCoordinates(pos) 222 223 #setup movement 224 move_time = self.get_unit_velocity() 225 UnitClass.ensure_action_loaded(self._action_set_id, self._move_action) # lazy load move action 226 227 self._exact_model_coords1.set(self.position.x, self.position.y, 0) 228 self._fife_location1.setExactLayerCoordinates(self._exact_model_coords1) 229 self._exact_model_coords2.set(self._next_target.x, self._next_target.y, 0) 230 self._fife_location2.setExactLayerCoordinates(self._exact_model_coords2) 231 self._route = fife.Route(self._fife_location1, self._fife_location2) 232 # TODO/HACK the *5 provides slightly less flickery behavior of the moving 233 # objects. This should be fixed properly by using the fife pathfinder for 234 # the entire route and task 235 location_list = fife.LocationList([self._fife_location2] * 5) 236 self._route.setPath(location_list) 237 238 self.act(self._move_action) 239 diagonal = self._next_target.x != self.position.x and self._next_target.y != self.position.y 240 speed = float(self.session.timer.get_ticks(1)) / move_time[0] 241 action = self._instance.getCurrentAction().getId() 242 self._instance.follow(action, self._route, speed) 243 244 #self.log.debug("%s registering move tick in %s ticks", self, move_time[int(diagonal)]) 245 Scheduler().add_new_object(self._move_tick, self, move_time[int(diagonal)]) 246 247 # check if a conditional callback becomes true 248 for cond in list(self._conditional_callbacks.keys()): # iterate of copy of keys to be able to delete 249 if cond(): 250 # start callback when this function is done 251 Scheduler().add_new_object(self._conditional_callbacks[cond], self) 252 del self._conditional_callbacks[cond]
253
254 - def teleport(self, destination, callback=None, destination_in_building=False):
255 """Like move, but nearly instantaneous""" 256 if hasattr(destination, "position"): 257 destination_coord = destination.position.center.to_tuple() 258 else: 259 destination_coord = destination 260 self.move(destination, callback=callback, destination_in_building=destination_in_building, path=[destination_coord])
261
262 - def add_move_callback(self, callback):
263 """Registers callback to be executed when movement of unit finishes. 264 This has no effect if the unit isn't moving.""" 265 if self.is_moving(): 266 self.move_callbacks.append(callback)
267
268 - def add_blocked_callback(self, blocked_callback):
269 """Registers callback to be executed when movement of the unit gets blocked.""" 270 self.blocked_callbacks.append(blocked_callback)
271
272 - def add_conditional_callback(self, condition, callback):
273 """Adds a callback, that gets called, if, at any time of the movement, the condition becomes 274 True. The condition is checked every move_tick. After calling the callback, it is removed.""" 275 assert callable(condition) 276 assert callable(callback) 277 self._conditional_callbacks[condition] = callback
278
279 - def get_unit_velocity(self):
280 """Returns the number of ticks that it takes to do a straight (i.e. vertical or horizontal) 281 or diagonal movement as a tuple in this order. 282 @return: (int, int) 283 """ 284 tile = self.session.world.get_tile(self.position) 285 if self.id in tile.velocity: 286 return tile.velocity[self.id] 287 else: 288 return (12, 17) # standard values
289
290 - def get_move_target(self):
291 return self.path.get_move_target()
292
293 - def save(self, db):
294 super().save(db) 295 # NOTE: _move_action is currently not yet saved and neither is blocked_callback. 296 self.path.save(db, self.worldid)
297
298 - def load(self, db, worldid):
299 super().load(db, worldid) 300 x, y = db("SELECT x, y FROM unit WHERE rowid = ?", worldid)[0] 301 self.__init(x, y) 302 path_loaded = self.path.load(db, worldid) 303 if path_loaded: 304 self.__is_moving = True 305 self._setup_move() 306 Scheduler().add_new_object(self._move_tick, self, run_in=0)
307